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.
Files changed (40) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +22 -0
  4. package/README.md +199 -0
  5. package/agents/architect.md +45 -0
  6. package/agents/debugger.md +57 -0
  7. package/agents/git-committer.md +52 -0
  8. package/agents/implementer.md +35 -0
  9. package/agents/reviewer.md +57 -0
  10. package/agents/security-auditor.md +109 -0
  11. package/agents/verifier.md +44 -0
  12. package/bin/draht-tools.cjs +1067 -0
  13. package/cli.mjs +348 -0
  14. package/commands/atomic-commit.md +61 -0
  15. package/commands/discuss-phase.md +54 -0
  16. package/commands/execute-phase.md +111 -0
  17. package/commands/fix.md +50 -0
  18. package/commands/init-project.md +65 -0
  19. package/commands/map-codebase.md +52 -0
  20. package/commands/new-project.md +73 -0
  21. package/commands/next-milestone.md +49 -0
  22. package/commands/orchestrate.md +58 -0
  23. package/commands/pause-work.md +38 -0
  24. package/commands/plan-phase.md +107 -0
  25. package/commands/progress.md +30 -0
  26. package/commands/quick.md +50 -0
  27. package/commands/resume-work.md +35 -0
  28. package/commands/review.md +55 -0
  29. package/commands/verify-work.md +72 -0
  30. package/hooks/hooks.json +26 -0
  31. package/package.json +50 -0
  32. package/scripts/gsd-post-phase.cjs +133 -0
  33. package/scripts/gsd-post-task.cjs +165 -0
  34. package/scripts/gsd-pre-execute.cjs +146 -0
  35. package/scripts/gsd-quality-gate.cjs +252 -0
  36. package/scripts/prompt-context.cjs +36 -0
  37. package/scripts/session-start.cjs +52 -0
  38. package/skills/ddd-workflow/SKILL.md +108 -0
  39. package/skills/gsd-workflow/SKILL.md +111 -0
  40. package/skills/tdd-workflow/SKILL.md +115 -0
@@ -0,0 +1,1067 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Draht Tools — CLI for the Get Shit Done methodology.
6
+ * Manages .planning/ directory structure, state tracking, and git integration.
7
+ */
8
+
9
+ const fs = require("node:fs");
10
+ const path = require("node:path");
11
+ const { execSync } = require("node:child_process");
12
+
13
+ const PLANNING_DIR = ".planning";
14
+ const BANNER_WIDTH = 55;
15
+
16
+ // ============================================================================
17
+ // Helpers
18
+ // ============================================================================
19
+
20
+ function banner(stage) {
21
+ const line = "━".repeat(BANNER_WIDTH);
22
+ return `${line}\n DRAHT ► ${stage}\n${line}`;
23
+ }
24
+
25
+ function planningPath(...segments) {
26
+ return path.join(process.cwd(), PLANNING_DIR, ...segments);
27
+ }
28
+
29
+ function ensureDir(dir) {
30
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+
33
+ function readJson(filePath) {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function readMd(filePath) {
42
+ try {
43
+ return fs.readFileSync(filePath, "utf-8");
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function writeMd(filePath, content) {
50
+ ensureDir(path.dirname(filePath));
51
+ fs.writeFileSync(filePath, content, "utf-8");
52
+ }
53
+
54
+ function writeJson(filePath, data) {
55
+ ensureDir(path.dirname(filePath));
56
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
57
+ }
58
+
59
+ function timestamp() {
60
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
61
+ }
62
+
63
+ function dateStamp() {
64
+ return new Date().toISOString().slice(0, 10);
65
+ }
66
+
67
+ function slugify(text, maxLen = 40) {
68
+ return text
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .replace(/^-|-$/g, "")
72
+ .slice(0, maxLen);
73
+ }
74
+
75
+ function padNum(n, digits = 2) {
76
+ return String(n).padStart(digits, "0");
77
+ }
78
+
79
+ function hasGit() {
80
+ try {
81
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function gitCommit(message) {
89
+ if (!hasGit()) return null;
90
+ try {
91
+ execSync(`git add ${PLANNING_DIR}/`, { stdio: "ignore" });
92
+ execSync(`git commit -m ${JSON.stringify(message)}`, { stdio: "ignore" });
93
+ const hash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
94
+ return hash;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function gitCommitAll(message) {
101
+ if (!hasGit()) return null;
102
+ try {
103
+ execSync("git add -A", { stdio: "ignore" });
104
+ execSync(`git commit -m ${JSON.stringify(message)}`, { stdio: "ignore" });
105
+ const hash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
106
+ return hash;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function getPhaseDir(phaseNum) {
113
+ const dir = planningPath("phases");
114
+ if (!fs.existsSync(dir)) return null;
115
+ const entries = fs.readdirSync(dir);
116
+ const match = entries.find((e) => e.startsWith(padNum(phaseNum) + "-"));
117
+ return match ? path.join(dir, match) : null;
118
+ }
119
+
120
+ function getPhaseSlug(phaseNum) {
121
+ const dir = getPhaseDir(phaseNum);
122
+ if (!dir) return null;
123
+ return path.basename(dir).replace(/^\d+-/, "");
124
+ }
125
+
126
+ function parsePhaseFromRoadmap(roadmapContent, phaseNum) {
127
+ const regex = new RegExp(
128
+ `## Phase ${phaseNum}:\\s*(.+?)\\s*—\\s*\`(\\w+)\``,
129
+ "m"
130
+ );
131
+ const match = roadmapContent.match(regex);
132
+ if (match) return { name: match[1].trim(), status: match[2] };
133
+ // Try without status
134
+ const regex2 = new RegExp(`## Phase ${phaseNum}:\\s*(.+?)\\n`, "m");
135
+ const match2 = roadmapContent.match(regex2);
136
+ if (match2) return { name: match2[1].trim(), status: "unknown" };
137
+ return null;
138
+ }
139
+
140
+ function getState() {
141
+ return readMd(planningPath("STATE.md"));
142
+ }
143
+
144
+ function getRoadmap() {
145
+ return readMd(planningPath("ROADMAP.md"));
146
+ }
147
+
148
+ function getConfig() {
149
+ return readJson(planningPath("config.json")) || {
150
+ mode: "yolo",
151
+ depth: "standard",
152
+ workflow: { research: true, plan_check: true, verifier: true },
153
+ git: { commit_docs: true },
154
+ };
155
+ }
156
+
157
+ // ============================================================================
158
+ // Commands
159
+ // ============================================================================
160
+
161
+ const commands = {};
162
+
163
+ // --- init ---
164
+ commands.init = function () {
165
+ const exists = fs.existsSync(planningPath("PROJECT.md"));
166
+ if (exists) {
167
+ console.log("Project already initialized. Use `draht-tools progress` to see status.");
168
+ process.exit(1);
169
+ }
170
+ ensureDir(planningPath());
171
+ const hasCode =
172
+ fs.existsSync("package.json") ||
173
+ fs.existsSync("src") ||
174
+ fs.existsSync("Cargo.toml") ||
175
+ fs.existsSync("go.mod");
176
+ console.log(banner("INIT"));
177
+ console.log(`\nPlanning directory: ${PLANNING_DIR}/`);
178
+ if (hasCode) {
179
+ console.log("\n⚠️ Existing code detected. Consider running: draht-tools map-codebase");
180
+ }
181
+ if (!hasGit()) {
182
+ console.log("\n⚠️ No git repository. Consider running: git init");
183
+ }
184
+ console.log("\nReady for project initialization.");
185
+ };
186
+
187
+ // --- map-codebase ---
188
+ commands["map-codebase"] = function (dir) {
189
+ const cwd = dir || process.cwd();
190
+ const outDir = planningPath("codebase");
191
+ ensureDir(outDir);
192
+
193
+ console.log(banner("MAPPING CODEBASE"));
194
+
195
+ // Gather file tree
196
+ let tree = "";
197
+ try {
198
+ tree = execSync(
199
+ `find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.planning/*' | head -200`,
200
+ { cwd, encoding: "utf-8" }
201
+ );
202
+ } catch { /* empty */ }
203
+
204
+ // Gather package info
205
+ let pkgJson = null;
206
+ try {
207
+ pkgJson = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
208
+ } catch { /* empty */ }
209
+
210
+ // Write STACK.md
211
+ writeMd(path.join(outDir, "STACK.md"), `# Technology Stack\n\nGenerated: ${timestamp()}\n\n## File Tree (first 200 files)\n\`\`\`\n${tree}\`\`\`\n\n## Package Info\n\`\`\`json\n${pkgJson ? JSON.stringify({ name: pkgJson.name, dependencies: pkgJson.dependencies, devDependencies: pkgJson.devDependencies }, null, 2) : "No package.json found"}\n\`\`\`\n\n## TODO\n- [ ] Fill in languages, versions, frameworks\n- [ ] Document build tools and runtime\n`);
212
+
213
+ // Write placeholder files
214
+ writeMd(path.join(outDir, "ARCHITECTURE.md"), `# Architecture\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Document file/directory patterns\n- [ ] Map module boundaries\n- [ ] Describe data flow\n`);
215
+
216
+ writeMd(path.join(outDir, "CONVENTIONS.md"), `# Conventions\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Document code style patterns\n- [ ] Document testing patterns\n- [ ] Document error handling approach\n`);
217
+
218
+ writeMd(path.join(outDir, "CONCERNS.md"), `# Concerns\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Identify technical debt\n- [ ] Flag security concerns\n- [ ] Note missing tests\n`);
219
+
220
+ // Domain model extraction
221
+ let domainHints = "";
222
+ try {
223
+ // Extract types/interfaces as potential entities
224
+ const types = execSync(
225
+ `grep -rn 'export\\s\\+\\(interface\\|type\\|class\\)' --include='*.ts' --include='*.go' . 2>/dev/null | grep -v node_modules | grep -v dist | head -50`,
226
+ { cwd, encoding: "utf-8" }
227
+ ).trim();
228
+ if (types) domainHints += `## Types/Interfaces (potential entities)\n\`\`\`\n${types}\n\`\`\`\n\n`;
229
+ } catch { /* empty */ }
230
+ try {
231
+ // Extract directory structure as potential bounded contexts
232
+ const dirs = execSync(
233
+ `find . -type d -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | sort`,
234
+ { cwd, encoding: "utf-8" }
235
+ ).trim();
236
+ if (dirs) domainHints += `## Directory Structure (potential bounded contexts)\n\`\`\`\n${dirs}\n\`\`\`\n`;
237
+ } catch { /* empty */ }
238
+
239
+ writeMd(path.join(outDir, "DOMAIN-HINTS.md"), `# Domain Model Hints\n\nGenerated: ${timestamp()}\n\nExtracted from codebase to help identify domain model.\n\n${domainHints}\n## TODO\n- [ ] Identify entities vs value objects\n- [ ] Map bounded contexts from directory structure\n- [ ] Define ubiquitous language glossary\n`);
240
+
241
+ console.log(`\nCreated:\n ${outDir}/STACK.md\n ${outDir}/ARCHITECTURE.md\n ${outDir}/CONVENTIONS.md\n ${outDir}/CONCERNS.md\n ${outDir}/DOMAIN-HINTS.md`);
242
+ console.log("\n→ Review and fill in the TODOs, then run: draht-tools commit-docs \"map existing codebase\"");
243
+ };
244
+
245
+ // --- create-project ---
246
+ commands["create-project"] = function (...args) {
247
+ const name = args.join(" ") || "Untitled Project";
248
+ const tmpl = `# Project: ${name}\n\n## Vision\n[Fill in]\n\n## Problem\n[What problem does this solve?]\n\n## Target User\n[Who is this for?]\n\n## Tech Stack\n[Languages, frameworks, infrastructure]\n\n## Domain Model\n\n### Bounded Contexts\n- **[Context Name]** — [responsibility]\n\n### Entities\n- **[Entity]** — [description, key attributes]\n\n### Value Objects\n- **[ValueObject]** — [description]\n\n### Aggregates\n- **[Aggregate Root]** — [entities it owns]\n\n### Ubiquitous Language\n| Term | Definition |\n|------|------------|\n| [Term] | [What it means in this domain] |\n\n## Constraints\n[Budget, timeline, platform requirements]\n\n## Success Criteria\n[How do we know this works?]\n\n---\nCreated: ${timestamp()}\n`;
249
+ writeMd(planningPath("PROJECT.md"), tmpl);
250
+ console.log(`Created: ${PLANNING_DIR}/PROJECT.md`);
251
+ };
252
+
253
+ // --- create-requirements ---
254
+ commands["create-requirements"] = function () {
255
+ const tmpl = `# Requirements\n\n## v1 — Must Have\n- [ ] [Requirement 1] *(Context: [bounded context])*\n\n## v2 — Nice to Have\n- [ ] [Requirement 1] *(Context: [bounded context])*\n\n## Bounded Context Mapping\n| Requirement | Bounded Context | Aggregate |\n|-------------|----------------|----------|\n| R1 | [context] | [aggregate] |\n\n## Out of Scope\n- [Explicitly excluded]\n\n---\nCreated: ${timestamp()}\n`;
256
+ writeMd(planningPath("REQUIREMENTS.md"), tmpl);
257
+ console.log(`Created: ${PLANNING_DIR}/REQUIREMENTS.md`);
258
+ };
259
+
260
+ // --- create-roadmap ---
261
+ commands["create-roadmap"] = function () {
262
+ const tmpl = `# Roadmap\n\n## Phase 1: [Name] — \`pending\`\n**Goal:** [Outcome, not activity]\n**Requirements:** [Which requirements this covers]\n**Acceptance:** [How we know it's done]\n\n---\nCreated: ${timestamp()}\n`;
263
+ writeMd(planningPath("ROADMAP.md"), tmpl);
264
+ console.log(`Created: ${PLANNING_DIR}/ROADMAP.md`);
265
+ };
266
+
267
+ // --- create-domain-model ---
268
+ commands["create-domain-model"] = function () {
269
+ const project = readMd(planningPath("PROJECT.md"));
270
+ if (!project) { console.error("No PROJECT.md found — run create-project first"); process.exit(1); }
271
+
272
+ const tmpl = `# Domain Model\n\n## Bounded Contexts\n[Extract from PROJECT.md — identify distinct areas of responsibility]\n\n## Context Map\n[How bounded contexts interact — upstream/downstream, shared kernel, etc.]\n\n## Entities\n[Core domain objects with identity]\n\n## Value Objects\n[Immutable objects defined by attributes]\n\n## Aggregates\n[Cluster of entities with a root — transactional boundary]\n\n## Domain Events\n[Things that happen in the domain]\n\n## Ubiquitous Language Glossary\n| Term | Context | Definition |\n|------|---------|------------|\n| [term] | [context] | [definition] |\n\n---\nGenerated from PROJECT.md: ${timestamp()}\n`;
273
+ writeMd(planningPath("DOMAIN-MODEL.md"), tmpl);
274
+ console.log(`Created: ${PLANNING_DIR}/DOMAIN-MODEL.md`);
275
+ console.log("Fill in the domain model based on PROJECT.md context.");
276
+ };
277
+
278
+ // --- init-state ---
279
+ commands["init-state"] = function () {
280
+ const tmpl = `# State\n\n## Current Phase: 1\n## Status: initialized\n\n## Decisions\n(none yet)\n\n## Blockers\nNone.\n\n## Quick Tasks Completed\n(none)\n\n## Last Activity: ${timestamp()}\n`;
281
+ writeMd(planningPath("STATE.md"), tmpl);
282
+ writeJson(planningPath("config.json"), getConfig());
283
+ console.log(`Created: ${PLANNING_DIR}/STATE.md`);
284
+ console.log(`Created: ${PLANNING_DIR}/config.json`);
285
+ };
286
+
287
+ // --- phase-info ---
288
+ commands["phase-info"] = function (n) {
289
+ const num = parseInt(n, 10);
290
+ if (!num) { console.error("Usage: draht-tools phase-info N"); process.exit(1); }
291
+
292
+ const roadmap = getRoadmap();
293
+ if (!roadmap) { console.error("No ROADMAP.md found"); process.exit(1); }
294
+
295
+ const info = parsePhaseFromRoadmap(roadmap, num);
296
+ const phaseDir = getPhaseDir(num);
297
+ const contextFile = phaseDir ? path.join(phaseDir, `${padNum(num)}-CONTEXT.md`) : null;
298
+ const context = contextFile ? readMd(contextFile) : null;
299
+
300
+ console.log(banner(`PHASE ${num} INFO`));
301
+ if (info) console.log(`\nName: ${info.name}\nStatus: ${info.status}`);
302
+ if (phaseDir) console.log(`Directory: ${phaseDir}`);
303
+ if (context) console.log(`\n--- Context ---\n${context}`);
304
+ else console.log("\nNo context captured yet. Run: /discuss-phase " + num);
305
+ };
306
+
307
+ // --- save-context ---
308
+ // Supports stdin: echo "content" | draht-tools save-context N
309
+ commands["save-context"] = async function (n, ...rest) {
310
+ const num = parseInt(n, 10);
311
+ if (!num) { console.error("Usage: draht-tools save-context N"); process.exit(1); }
312
+
313
+ const slug = getPhaseSlug(num) || `phase-${num}`;
314
+ const dir = planningPath("phases", `${padNum(num)}-${slug}`);
315
+ ensureDir(dir);
316
+
317
+ const contextPath = path.join(dir, `${padNum(num)}-CONTEXT.md`);
318
+
319
+ // Check for stdin content (piped input)
320
+ let stdinContent = "";
321
+ if (!process.stdin.isTTY) {
322
+ stdinContent = await new Promise((resolve) => {
323
+ let data = "";
324
+ process.stdin.setEncoding("utf-8");
325
+ process.stdin.on("data", (chunk) => { data += chunk; });
326
+ process.stdin.on("end", () => { resolve(data.trim()); });
327
+ });
328
+ }
329
+
330
+ if (stdinContent) {
331
+ writeMd(contextPath, stdinContent);
332
+ console.log(`Created: ${contextPath}`);
333
+ } else if (fs.existsSync(contextPath)) {
334
+ console.log(`Context already exists at ${contextPath}`);
335
+ console.log("Edit it directly or pass content via stdin.");
336
+ } else {
337
+ const tmpl = `# Phase ${num} Context\n\n## Domain Boundary\n[What this phase covers]\n\n## Decisions\n[Captured during discussion]\n\n## Claude's Discretion\n[Areas where Claude can decide]\n\n## Deferred Ideas\n[Saved for later]\n\n---\nCreated: ${timestamp()}\n`;
338
+ writeMd(contextPath, tmpl);
339
+ console.log(`Created: ${contextPath}`);
340
+ }
341
+ };
342
+
343
+ // --- load-phase-context ---
344
+ commands["load-phase-context"] = function (n) {
345
+ const num = parseInt(n, 10);
346
+ if (!num) { console.error("Usage: draht-tools load-phase-context N"); process.exit(1); }
347
+
348
+ const files = [];
349
+ const project = readMd(planningPath("PROJECT.md"));
350
+ if (project) files.push({ name: "PROJECT.md", content: project });
351
+
352
+ const reqs = readMd(planningPath("REQUIREMENTS.md"));
353
+ if (reqs) files.push({ name: "REQUIREMENTS.md", content: reqs });
354
+
355
+ const roadmap = getRoadmap();
356
+ if (roadmap) files.push({ name: "ROADMAP.md", content: roadmap });
357
+
358
+ const phaseDir = getPhaseDir(num);
359
+ if (phaseDir) {
360
+ const context = readMd(path.join(phaseDir, `${padNum(num)}-CONTEXT.md`));
361
+ if (context) files.push({ name: `${padNum(num)}-CONTEXT.md`, content: context });
362
+ }
363
+
364
+ // Codebase docs
365
+ const cbDir = planningPath("codebase");
366
+ if (fs.existsSync(cbDir)) {
367
+ for (const f of fs.readdirSync(cbDir)) {
368
+ if (f.endsWith(".md")) {
369
+ files.push({ name: `codebase/${f}`, content: readMd(path.join(cbDir, f)) });
370
+ }
371
+ }
372
+ }
373
+
374
+ // Research docs
375
+ const resDir = planningPath("research");
376
+ if (fs.existsSync(resDir)) {
377
+ for (const f of fs.readdirSync(resDir)) {
378
+ if (f.endsWith(".md")) {
379
+ files.push({ name: `research/${f}`, content: readMd(path.join(resDir, f)) });
380
+ }
381
+ }
382
+ }
383
+
384
+ console.log(banner(`PHASE ${num} CONTEXT`));
385
+ for (const f of files) {
386
+ console.log(`\n=== ${f.name} ===\n${f.content}`);
387
+ }
388
+ };
389
+
390
+ // --- create-plan ---
391
+ // Supports stdin: echo "content" | draht-tools create-plan N P [title]
392
+ commands["create-plan"] = async function (n, p, ...titleWords) {
393
+ const phaseNum = parseInt(n, 10);
394
+ const planNum = parseInt(p, 10);
395
+ if (!phaseNum || !planNum) { console.error("Usage: draht-tools create-plan N P [title]"); process.exit(1); }
396
+
397
+ const slug = getPhaseSlug(phaseNum) || `phase-${phaseNum}`;
398
+ const dir = planningPath("phases", `${padNum(phaseNum)}-${slug}`);
399
+ ensureDir(dir);
400
+
401
+ const title = titleWords.join(" ") || `Plan ${planNum}`;
402
+ const planPath = path.join(dir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);
403
+
404
+ // Check for stdin content (piped input)
405
+ let stdinContent = "";
406
+ if (!process.stdin.isTTY) {
407
+ stdinContent = await new Promise((resolve) => {
408
+ let data = "";
409
+ process.stdin.setEncoding("utf-8");
410
+ process.stdin.on("data", (chunk) => { data += chunk; });
411
+ process.stdin.on("end", () => { resolve(data.trim()); });
412
+ });
413
+ }
414
+
415
+ let content;
416
+ if (stdinContent) {
417
+ // Use stdin content directly
418
+ content = stdinContent;
419
+ } else {
420
+ // Generate template
421
+ content = `---
422
+ phase: ${phaseNum}
423
+ plan: ${planNum}
424
+ depends_on: []
425
+ must_haves:
426
+ - "[Observable truth this plan delivers]"
427
+ ---
428
+
429
+ # Phase ${phaseNum}, Plan ${planNum}: ${title}
430
+
431
+ ## Goal
432
+ [What this plan achieves from user perspective]
433
+
434
+ ## Context
435
+ [Key decisions that affect this plan]
436
+
437
+ ## Tasks
438
+
439
+ <task type="auto">
440
+ <n>[Task name]</n>
441
+ <files>[affected files]</files>
442
+ <test>[Write tests first — what should pass when done]</test>
443
+ <action>
444
+ [Implementation to make tests pass]
445
+ </action>
446
+ <refactor>[Optional cleanup after green]</refactor>
447
+ <verify>[How to verify]</verify>
448
+ <done>[What "done" looks like]</done>
449
+ </task>
450
+
451
+ ---
452
+ Created: ${timestamp()}
453
+ `;
454
+ }
455
+
456
+ writeMd(planPath, content);
457
+ console.log(`Created: ${planPath}`);
458
+ };
459
+
460
+ // --- discover-plans ---
461
+ commands["discover-plans"] = function (n) {
462
+ const num = parseInt(n, 10);
463
+ if (!num) { console.error("Usage: draht-tools discover-plans N"); process.exit(1); }
464
+
465
+ const phaseDir = getPhaseDir(num);
466
+ if (!phaseDir) { console.error(`Phase ${num} directory not found`); process.exit(1); }
467
+
468
+ const files = fs.readdirSync(phaseDir).sort();
469
+ const plans = files.filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
470
+ const summaries = files.filter((f) => f.endsWith("-SUMMARY.md"));
471
+ const fixPlans = files.filter((f) => f.includes("FIX-PLAN.md"));
472
+
473
+ const completedPlanNums = new Set(
474
+ summaries.map((s) => {
475
+ const match = s.match(/\d+-(\d+)-SUMMARY/);
476
+ return match ? match[1] : null;
477
+ }).filter(Boolean)
478
+ );
479
+
480
+ const incomplete = plans.filter((p) => {
481
+ const match = p.match(/\d+-(\d+)-PLAN/);
482
+ return match ? !completedPlanNums.has(match[1]) : true;
483
+ });
484
+
485
+ console.log(banner(`PHASE ${num} PLANS`));
486
+ console.log(`\nTotal plans: ${plans.length}`);
487
+ console.log(`Completed: ${summaries.length}`);
488
+ console.log(`Remaining: ${incomplete.length}`);
489
+ console.log(`Fix plans: ${fixPlans.length}`);
490
+
491
+ if (incomplete.length > 0) {
492
+ console.log(`\nIncomplete plans:`);
493
+ for (const p of incomplete) console.log(` - ${p}`);
494
+ }
495
+
496
+ // Parse dependencies for ordering
497
+ const planData = plans.map((p) => {
498
+ const content = readMd(path.join(phaseDir, p));
499
+ const depsMatch = content?.match(/depends_on:\s*\[(.*?)\]/);
500
+ const deps = depsMatch ? depsMatch[1].split(",").map((d) => d.trim()).filter(Boolean) : [];
501
+ return { file: p, deps };
502
+ });
503
+
504
+ // Output as JSON for programmatic use
505
+ console.log(`\n--- JSON ---`);
506
+ console.log(JSON.stringify({ plans: planData, incomplete, fixPlans }, null, 2));
507
+ };
508
+
509
+ // --- read-plan ---
510
+ commands["read-plan"] = function (n, p) {
511
+ const phaseNum = parseInt(n, 10);
512
+ const planNum = parseInt(p, 10);
513
+ if (!phaseNum || !planNum) { console.error("Usage: draht-tools read-plan N P"); process.exit(1); }
514
+
515
+ const phaseDir = getPhaseDir(phaseNum);
516
+ if (!phaseDir) { console.error(`Phase ${phaseNum} not found`); process.exit(1); }
517
+
518
+ const planFile = `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`;
519
+ const content = readMd(path.join(phaseDir, planFile));
520
+ if (!content) { console.error(`Plan file not found: ${planFile}`); process.exit(1); }
521
+
522
+ console.log(content);
523
+ };
524
+
525
+ // --- validate-plans ---
526
+ commands["validate-plans"] = function (n) {
527
+ const num = parseInt(n, 10);
528
+ if (!num) { console.error("Usage: draht-tools validate-plans N"); process.exit(1); }
529
+
530
+ const phaseDir = getPhaseDir(num);
531
+ if (!phaseDir) { console.error(`Phase ${num} not found`); process.exit(1); }
532
+
533
+ const files = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-PLAN.md"));
534
+ const issues = [];
535
+
536
+ for (const file of files) {
537
+ const content = readMd(path.join(phaseDir, file));
538
+ if (!content) continue;
539
+
540
+ // Check for required elements
541
+ if (!content.includes("<task")) issues.push(`${file}: No <task> elements found`);
542
+ if (!content.includes("<verify>")) issues.push(`${file}: Missing <verify> in tasks`);
543
+ if (!content.includes("<done>")) issues.push(`${file}: Missing <done> in tasks`);
544
+ if (!content.includes("must_haves")) issues.push(`${file}: Missing must_haves in frontmatter`);
545
+
546
+ // Count tasks
547
+ const taskCount = (content.match(/<task/g) || []).length;
548
+ if (taskCount > 5) issues.push(`${file}: ${taskCount} tasks (max recommended: 5)`);
549
+ if (taskCount === 0) issues.push(`${file}: No tasks defined`);
550
+ }
551
+
552
+ console.log(banner(`VALIDATE PHASE ${num}`));
553
+ console.log(`\nPlans checked: ${files.length}`);
554
+ if (issues.length === 0) {
555
+ console.log("✅ All plans valid");
556
+ } else {
557
+ console.log(`\n⚠️ ${issues.length} issue(s):`);
558
+ for (const issue of issues) console.log(` - ${issue}`);
559
+ }
560
+ };
561
+
562
+ // --- commit-task ---
563
+ commands["commit-task"] = function (n, p, t, ...desc) {
564
+ const phaseNum = padNum(parseInt(n, 10));
565
+ const planNum = padNum(parseInt(p, 10));
566
+ const taskNum = t || "1";
567
+ const description = desc.join(" ") || "implement task";
568
+
569
+ const hash = gitCommitAll(`feat(${phaseNum}-${planNum}): ${description}`);
570
+ if (hash) {
571
+ console.log(`Committed: ${hash} — feat(${phaseNum}-${planNum}): ${description}`);
572
+ // TDD check: warn if no test files in commit
573
+ try {
574
+ const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, { encoding: "utf-8" }).trim();
575
+ const hasTests = files.split("\n").some((f) => /\.(test|spec)\.(ts|tsx|js|jsx)$|_test\.(go|ts)$/.test(f));
576
+ if (!hasTests) {
577
+ console.log("⚠️ No test files in this commit — TDD requires tests first");
578
+ }
579
+ } catch { /* ignore */ }
580
+ } else {
581
+ console.log("Nothing to commit (or no git)");
582
+ }
583
+ };
584
+
585
+ // --- write-summary ---
586
+ // Supports stdin: echo "content" | draht-tools write-summary N P
587
+ commands["write-summary"] = async function (n, p) {
588
+ const phaseNum = parseInt(n, 10);
589
+ const planNum = parseInt(p, 10);
590
+ if (!phaseNum || !planNum) { console.error("Usage: draht-tools write-summary N P"); process.exit(1); }
591
+
592
+ const phaseDir = getPhaseDir(phaseNum);
593
+ if (!phaseDir) { console.error(`Phase ${phaseNum} not found`); process.exit(1); }
594
+
595
+ const summaryPath = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-SUMMARY.md`);
596
+
597
+ // Check for stdin content (piped input)
598
+ let stdinContent = "";
599
+ if (!process.stdin.isTTY) {
600
+ stdinContent = await new Promise((resolve) => {
601
+ let data = "";
602
+ process.stdin.setEncoding("utf-8");
603
+ process.stdin.on("data", (chunk) => { data += chunk; });
604
+ process.stdin.on("end", () => { resolve(data.trim()); });
605
+ });
606
+ }
607
+
608
+ let content;
609
+ if (stdinContent) {
610
+ content = stdinContent;
611
+ } else {
612
+ content = `# Phase ${phaseNum}, Plan ${planNum} Summary\n\n## Completed Tasks\n| # | Task | Status | Commit |\n|---|------|--------|--------|\n| 1 | [task] | ✅ Done | [hash] |\n\n## Files Changed\n- [files]\n\n## Verification Results\n- [results]\n\n## Notes\n[deviations, decisions]\n\n---\nCompleted: ${timestamp()}\n`;
613
+ }
614
+
615
+ writeMd(summaryPath, content);
616
+ console.log(`Created: ${summaryPath}`);
617
+ };
618
+
619
+ // --- verify-phase ---
620
+ commands["verify-phase"] = function (n) {
621
+ const num = parseInt(n, 10);
622
+ if (!num) { console.error("Usage: draht-tools verify-phase N"); process.exit(1); }
623
+
624
+ const phaseDir = getPhaseDir(num);
625
+ if (!phaseDir) { console.error(`Phase ${num} not found`); process.exit(1); }
626
+
627
+ const plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
628
+ const summaries = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-SUMMARY.md"));
629
+
630
+ console.log(banner(`VERIFY PHASE ${num}`));
631
+ console.log(`\nPlans: ${plans.length}`);
632
+ console.log(`Summaries: ${summaries.length}`);
633
+
634
+ if (summaries.length >= plans.length) {
635
+ console.log("\n✅ All plans have summaries — phase execution complete");
636
+ // Write verification file
637
+ const verPath = path.join(phaseDir, `${padNum(num)}-VERIFICATION.md`);
638
+ writeMd(verPath, `# Phase ${num} Verification\n\nAll ${plans.length} plans executed.\nVerified: ${timestamp()}\n`);
639
+ } else {
640
+ console.log(`\n⚠️ ${plans.length - summaries.length} plan(s) still incomplete`);
641
+ }
642
+ };
643
+
644
+ // --- extract-deliverables ---
645
+ commands["extract-deliverables"] = function (n) {
646
+ const num = parseInt(n, 10);
647
+ if (!num) { console.error("Usage: draht-tools extract-deliverables N"); process.exit(1); }
648
+
649
+ const phaseDir = getPhaseDir(num);
650
+ if (!phaseDir) { console.error(`Phase ${num} not found`); process.exit(1); }
651
+
652
+ const plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-PLAN.md"));
653
+ const deliverables = [];
654
+
655
+ for (const planFile of plans) {
656
+ const content = readMd(path.join(phaseDir, planFile));
657
+ if (!content) continue;
658
+
659
+ // Extract must_haves
660
+ const mustHaveMatch = content.match(/must_haves:\s*\n((?:\s+-\s+.+\n?)*)/);
661
+ if (mustHaveMatch) {
662
+ const items = mustHaveMatch[1].match(/- ["']?(.+?)["']?\s*$/gm) || [];
663
+ for (const item of items) {
664
+ deliverables.push({ source: planFile, type: "must_have", text: item.replace(/^\s*-\s*["']?|["']?\s*$/g, "") });
665
+ }
666
+ }
667
+
668
+ // Extract <done> tags
669
+ const doneMatches = content.matchAll(/<done>([\s\S]*?)<\/done>/g);
670
+ for (const match of doneMatches) {
671
+ deliverables.push({ source: planFile, type: "done", text: match[1].trim() });
672
+ }
673
+ }
674
+
675
+ console.log(banner(`PHASE ${num} DELIVERABLES`));
676
+ console.log(`\nFound ${deliverables.length} testable items:\n`);
677
+ deliverables.forEach((d, i) => {
678
+ console.log(` ${i + 1}. [${d.type}] ${d.text} (from ${d.source})`);
679
+ });
680
+
681
+ console.log(`\n--- JSON ---`);
682
+ console.log(JSON.stringify(deliverables, null, 2));
683
+ };
684
+
685
+ // --- create-fix-plan ---
686
+ // Supports stdin: echo "content" | draht-tools create-fix-plan N P [issue]
687
+ commands["create-fix-plan"] = async function (n, p, ...issueWords) {
688
+ const phaseNum = parseInt(n, 10);
689
+ const planNum = parseInt(p, 10);
690
+ if (!phaseNum || !planNum) { console.error("Usage: draht-tools create-fix-plan N P [issue]"); process.exit(1); }
691
+
692
+ const slug = getPhaseSlug(phaseNum) || `phase-${phaseNum}`;
693
+ const dir = planningPath("phases", `${padNum(phaseNum)}-${slug}`);
694
+ ensureDir(dir);
695
+ const issue = issueWords.join(" ") || "Fix identified issues";
696
+
697
+ const fixPath = path.join(dir, `${padNum(phaseNum)}-${padNum(planNum)}-FIX-PLAN.md`);
698
+
699
+ // Check for stdin content (piped input)
700
+ let stdinContent = "";
701
+ if (!process.stdin.isTTY) {
702
+ stdinContent = await new Promise((resolve) => {
703
+ let data = "";
704
+ process.stdin.setEncoding("utf-8");
705
+ process.stdin.on("data", (chunk) => { data += chunk; });
706
+ process.stdin.on("end", () => { resolve(data.trim()); });
707
+ });
708
+ }
709
+
710
+ let content;
711
+ if (stdinContent) {
712
+ content = stdinContent;
713
+ } else {
714
+ content = `---
715
+ gap_closure: true
716
+ fixes_plan: ${planNum}
717
+ issue: "${issue}"
718
+ ---
719
+
720
+ # Fix Plan for Phase ${phaseNum}, Plan ${planNum}
721
+
722
+ ## Issue
723
+ ${issue}
724
+
725
+ ## Tasks
726
+
727
+ <task type="auto">
728
+ <n>[Fix description]</n>
729
+ <files>[affected files]</files>
730
+ <action>[Fix instructions]</action>
731
+ <verify>[How to verify fix]</verify>
732
+ <done>[What "fixed" looks like]</done>
733
+ </task>
734
+
735
+ ---
736
+ Created: ${timestamp()}
737
+ `;
738
+ }
739
+
740
+ writeMd(fixPath, content);
741
+ console.log(`Created: ${fixPath}`);
742
+ };
743
+
744
+ // --- write-uat ---
745
+ // Supports stdin: echo "content" | draht-tools write-uat N
746
+ commands["write-uat"] = async function (n) {
747
+ const num = parseInt(n, 10);
748
+ if (!num) { console.error("Usage: draht-tools write-uat N"); process.exit(1); }
749
+
750
+ const phaseDir = getPhaseDir(num);
751
+ if (!phaseDir) { console.error(`Phase ${num} not found`); process.exit(1); }
752
+
753
+ const uatPath = path.join(phaseDir, `${padNum(num)}-UAT.md`);
754
+
755
+ // Check for stdin content (piped input)
756
+ let stdinContent = "";
757
+ if (!process.stdin.isTTY) {
758
+ stdinContent = await new Promise((resolve) => {
759
+ let data = "";
760
+ process.stdin.setEncoding("utf-8");
761
+ process.stdin.on("data", (chunk) => { data += chunk; });
762
+ process.stdin.on("end", () => { resolve(data.trim()); });
763
+ });
764
+ }
765
+
766
+ let content;
767
+ if (stdinContent) {
768
+ content = stdinContent;
769
+ } else {
770
+ content = `# Phase ${num} User Acceptance Testing\n\n## Test Date: ${dateStamp()}\n\n## Results\n| # | Deliverable | Status | Notes |\n|---|-------------|--------|-------|\n| 1 | [description] | ✅ Pass | |\n\n## Summary\n- Passed: X/Y\n- Failed: 0/Y\n- Skipped: 0/Y\n\n## Fix Plans Created\n(none)\n`;
771
+ }
772
+
773
+ writeMd(uatPath, content);
774
+ console.log(`Created: ${uatPath}`);
775
+ };
776
+
777
+ // --- next-quick-number ---
778
+ commands["next-quick-number"] = function () {
779
+ const dir = planningPath("quick");
780
+ if (!fs.existsSync(dir)) { console.log("001"); return; }
781
+ const entries = fs.readdirSync(dir).sort();
782
+ const last = entries[entries.length - 1];
783
+ const num = last ? parseInt(last.match(/^(\d+)/)?.[1] || "0", 10) + 1 : 1;
784
+ console.log(padNum(num, 3));
785
+ };
786
+
787
+ // --- create-quick-plan ---
788
+ // Supports stdin: echo "content" | draht-tools create-quick-plan NNN [desc]
789
+ commands["create-quick-plan"] = async function (n, ...descWords) {
790
+ const num = padNum(parseInt(n, 10), 3);
791
+ const desc = descWords.join(" ") || "Quick task";
792
+ const slug = slugify(desc);
793
+ const dir = planningPath("quick", `${num}-${slug}`);
794
+ ensureDir(dir);
795
+
796
+ const planPath = path.join(dir, `${num}-PLAN.md`);
797
+
798
+ // Check for stdin content (piped input)
799
+ let stdinContent = "";
800
+ if (!process.stdin.isTTY) {
801
+ stdinContent = await new Promise((resolve) => {
802
+ let data = "";
803
+ process.stdin.setEncoding("utf-8");
804
+ process.stdin.on("data", (chunk) => { data += chunk; });
805
+ process.stdin.on("end", () => { resolve(data.trim()); });
806
+ });
807
+ }
808
+
809
+ let content;
810
+ if (stdinContent) {
811
+ // Use stdin content directly
812
+ content = stdinContent;
813
+ } else {
814
+ // Generate template
815
+ content = `# Quick Task ${num}: ${desc}\n\n## Tasks\n\n<task type="auto">\n <n>[Task]</n>\n <files>[files]</files>\n <action>[instructions]</action>\n <verify>[verify]</verify>\n <done>[done]</done>\n</task>\n\n---\nCreated: ${timestamp()}\n`;
816
+ }
817
+
818
+ writeMd(planPath, content);
819
+ console.log(`Created: ${planPath}`);
820
+ };
821
+
822
+ // --- write-quick-summary ---
823
+ // Supports stdin: echo "content" | draht-tools write-quick-summary NNN
824
+ commands["write-quick-summary"] = async function (n) {
825
+ const num = padNum(parseInt(n, 10), 3);
826
+ const dir = planningPath("quick");
827
+ if (!fs.existsSync(dir)) { console.error("No quick tasks directory"); process.exit(1); }
828
+ const match = fs.readdirSync(dir).find((e) => e.startsWith(num));
829
+ if (!match) { console.error(`Quick task ${num} not found`); process.exit(1); }
830
+
831
+ const summaryPath = path.join(dir, match, `${num}-SUMMARY.md`);
832
+
833
+ // Check for stdin content (piped input)
834
+ let stdinContent = "";
835
+ if (!process.stdin.isTTY) {
836
+ stdinContent = await new Promise((resolve) => {
837
+ let data = "";
838
+ process.stdin.setEncoding("utf-8");
839
+ process.stdin.on("data", (chunk) => { data += chunk; });
840
+ process.stdin.on("end", () => { resolve(data.trim()); });
841
+ });
842
+ }
843
+
844
+ let content;
845
+ if (stdinContent) {
846
+ content = stdinContent;
847
+ } else {
848
+ content = `# Quick Task ${num} Summary\n\n## Tasks Completed\n| # | Task | Status | Commit |\n|---|------|--------|--------|\n| 1 | [task] | ✅ Done | [hash] |\n\n## Files Changed\n- [files]\n\n---\nCompleted: ${timestamp()}\n`;
849
+ }
850
+
851
+ writeMd(summaryPath, content);
852
+ console.log(`Created: ${summaryPath}`);
853
+ };
854
+
855
+ // --- update-state ---
856
+ commands["update-state"] = function () {
857
+ const statePath = planningPath("STATE.md");
858
+ let state = readMd(statePath);
859
+ if (!state) { console.error("No STATE.md found"); process.exit(1); }
860
+
861
+ // Update last activity
862
+ state = state.replace(/## Last Activity:.*/, `## Last Activity: ${timestamp()}`);
863
+ writeMd(statePath, state);
864
+ console.log(`Updated: ${statePath}`);
865
+ };
866
+
867
+ // --- progress ---
868
+ commands.progress = function () {
869
+ const state = getState();
870
+ const roadmap = getRoadmap();
871
+ if (!state || !roadmap) {
872
+ console.log("No Draht project found. Run: draht-tools init");
873
+ process.exit(1);
874
+ }
875
+
876
+ console.log(banner("PROJECT STATUS"));
877
+
878
+ // Parse phases from roadmap
879
+ const phaseRegex = /## Phase (\d+):\s*(.+?)\s*—\s*`(\w+)`/g;
880
+ let match;
881
+ const phases = [];
882
+ while ((match = phaseRegex.exec(roadmap))) {
883
+ phases.push({ num: parseInt(match[1], 10), name: match[2], status: match[3] });
884
+ }
885
+
886
+ if (phases.length === 0) {
887
+ console.log("\nNo phases found in ROADMAP.md");
888
+ } else {
889
+ console.log("\nPhases:");
890
+ for (const phase of phases) {
891
+ const icon = phase.status === "complete" ? "✅" : phase.status === "in-progress" ? "🔄" : "⬜";
892
+ const phaseDir = getPhaseDir(phase.num);
893
+ let planInfo = "";
894
+ if (phaseDir) {
895
+ const plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
896
+ const summaries = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-SUMMARY.md"));
897
+ planInfo = ` (${summaries.length}/${plans.length} plans)`;
898
+ }
899
+ console.log(` ${icon} Phase ${phase.num}: ${phase.name} — ${phase.status}${planInfo}`);
900
+ }
901
+ }
902
+
903
+ // Quick tasks
904
+ const quickDir = planningPath("quick");
905
+ if (fs.existsSync(quickDir)) {
906
+ const quickCount = fs.readdirSync(quickDir).length;
907
+ console.log(`\nQuick Tasks: ${quickCount}`);
908
+ }
909
+
910
+ // Blockers from state
911
+ const blockerMatch = state.match(/## Blockers\n([\s\S]*?)(?=\n##|\n---|\Z)/);
912
+ if (blockerMatch) {
913
+ const blockers = blockerMatch[1].trim();
914
+ if (blockers && blockers !== "None." && blockers !== "(none)") {
915
+ console.log(`\n⚠️ Blockers:\n${blockers}`);
916
+ }
917
+ }
918
+
919
+ // Last activity
920
+ const lastMatch = state.match(/## Last Activity:\s*(.+)/);
921
+ if (lastMatch) console.log(`\nLast activity: ${lastMatch[1]}`);
922
+ };
923
+
924
+ // --- pause ---
925
+ commands.pause = function () {
926
+ const state = getState();
927
+ if (!state) { console.error("No STATE.md found"); process.exit(1); }
928
+
929
+ let gitStatus = "";
930
+ try { gitStatus = execSync("git status --porcelain", { encoding: "utf-8" }).trim(); } catch { /* empty */ }
931
+
932
+ const tmpl = `# Continue Here\n\n## Session Paused: ${timestamp()}\n\n## Current Position\n[Fill from STATE.md]\n\n## What Was Happening\n[Brief description]\n\n## Uncommitted Changes\n${gitStatus || "None"}\n\n## Next Steps\n1. [What to do next]\n\n## Open Questions\n- [Any unresolved decisions]\n`;
933
+ writeMd(planningPath("CONTINUE-HERE.md"), tmpl);
934
+ console.log(`Created: ${PLANNING_DIR}/CONTINUE-HERE.md`);
935
+ };
936
+
937
+ // --- resume ---
938
+ commands.resume = function () {
939
+ const continueFile = readMd(planningPath("CONTINUE-HERE.md"));
940
+ if (continueFile) {
941
+ console.log(banner("RESUMING WORK"));
942
+ console.log(`\n${continueFile}`);
943
+ } else {
944
+ const state = getState();
945
+ if (state) {
946
+ console.log(banner("RESUMING WORK (from STATE.md)"));
947
+ console.log(`\n${state}`);
948
+ } else {
949
+ console.log("No Draht project found.");
950
+ }
951
+ }
952
+ };
953
+
954
+ // --- commit-docs ---
955
+ commands["commit-docs"] = function (...msg) {
956
+ const message = msg.join(" ") || "update planning docs";
957
+ const hash = gitCommit(`docs: ${message}`);
958
+ if (hash) console.log(`Committed: ${hash} — docs: ${message}`);
959
+ else console.log("Nothing to commit (or no git)");
960
+ };
961
+
962
+ // --- research-phase ---
963
+ // Supports stdin: echo "content" | draht-tools research-phase N
964
+ commands["research-phase"] = async function (n) {
965
+ const num = parseInt(n, 10);
966
+ if (!num) { console.error("Usage: draht-tools research-phase N"); process.exit(1); }
967
+
968
+ const slug = getPhaseSlug(num) || `phase-${num}`;
969
+ const dir = planningPath("phases", `${padNum(num)}-${slug}`);
970
+ ensureDir(dir);
971
+
972
+ const resPath = path.join(dir, `${padNum(num)}-RESEARCH.md`);
973
+
974
+ // Check for stdin content (piped input)
975
+ let stdinContent = "";
976
+ if (!process.stdin.isTTY) {
977
+ stdinContent = await new Promise((resolve) => {
978
+ let data = "";
979
+ process.stdin.setEncoding("utf-8");
980
+ process.stdin.on("data", (chunk) => { data += chunk; });
981
+ process.stdin.on("end", () => { resolve(data.trim()); });
982
+ });
983
+ }
984
+
985
+ let content;
986
+ if (stdinContent) {
987
+ content = stdinContent;
988
+ } else {
989
+ content = `# Phase ${num} Research\n\nGenerated: ${timestamp()}\n\n## Best Practices\n[Fill in]\n\n## Patterns & Anti-Patterns\n[Fill in]\n\n## Library Recommendations\n[Fill in]\n\n## Edge Cases & Gotchas\n[Fill in]\n`;
990
+ }
991
+
992
+ writeMd(resPath, content);
993
+ console.log(`Created: ${resPath}`);
994
+ if (!stdinContent) console.log("→ Fill in research findings, then plan the phase.");
995
+ };
996
+
997
+ // ============================================================================
998
+ // Help & Dispatch
999
+ // ============================================================================
1000
+
1001
+ commands.help = function () {
1002
+ console.log(`
1003
+ Draht Tools — Get Shit Done CLI
1004
+
1005
+ Usage: draht-tools <command> [args]
1006
+
1007
+ Project Setup:
1008
+ init Check preconditions, create .planning/
1009
+ map-codebase [dir] Analyze existing codebase
1010
+ create-project [name] Create PROJECT.md
1011
+ create-requirements Create REQUIREMENTS.md
1012
+ create-domain-model Generate DOMAIN-MODEL.md from PROJECT.md
1013
+ create-roadmap Create ROADMAP.md
1014
+ init-state Create STATE.md + config.json
1015
+
1016
+ Phase Management:
1017
+ phase-info N Show phase details
1018
+ save-context N Create/show CONTEXT.md for phase
1019
+ load-phase-context N Load all context for planning
1020
+ research-phase N Create research template for phase
1021
+ create-plan N P [title] Create PLAN.md template
1022
+ discover-plans N List and order plans in a phase
1023
+ read-plan N P Output plan content
1024
+ validate-plans N Check plans for required elements
1025
+
1026
+ Execution:
1027
+ commit-task N P T [desc] Git commit for a task
1028
+ write-summary N P Create SUMMARY.md for completed plan
1029
+ verify-phase N Check all plans have summaries
1030
+
1031
+ Verification:
1032
+ extract-deliverables N List testable items from plans
1033
+ create-fix-plan N P [issue] Create FIX-PLAN.md for failed tests
1034
+ write-uat N Create UAT report
1035
+
1036
+ Quick Tasks:
1037
+ next-quick-number Get next quick task number
1038
+ create-quick-plan NNN [desc] Create quick task plan
1039
+ write-quick-summary NNN Create quick task summary
1040
+
1041
+ Session:
1042
+ pause Create CONTINUE-HERE.md
1043
+ resume Load last session state
1044
+ progress Show project status
1045
+ update-state Update STATE.md timestamp
1046
+
1047
+ Git:
1048
+ commit-docs [message] Commit .planning/ changes
1049
+ commit-task N P T [desc] Commit all changes as task
1050
+
1051
+ Version: 1.0.0
1052
+ `);
1053
+ };
1054
+
1055
+ // Dispatch
1056
+ const [cmd, ...args] = process.argv.slice(2);
1057
+
1058
+ (async () => {
1059
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
1060
+ commands.help();
1061
+ } else if (commands[cmd]) {
1062
+ await commands[cmd](...args);
1063
+ } else {
1064
+ console.error(`Unknown command: ${cmd}\nRun: draht-tools help`);
1065
+ process.exit(1);
1066
+ }
1067
+ })();