@draht/coding-agent 2026.3.4 → 2026.3.5

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 (76) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/core/prompt-templates.d.ts.map +1 -1
  3. package/dist/core/prompt-templates.js +4 -2
  4. package/dist/core/prompt-templates.js.map +1 -1
  5. package/dist/gsd/domain-validator.d.ts +18 -0
  6. package/dist/gsd/domain-validator.d.ts.map +1 -0
  7. package/dist/gsd/domain-validator.js +61 -0
  8. package/dist/gsd/domain-validator.js.map +1 -0
  9. package/dist/gsd/domain.d.ts +12 -0
  10. package/dist/gsd/domain.d.ts.map +1 -0
  11. package/dist/gsd/domain.js +113 -0
  12. package/dist/gsd/domain.js.map +1 -0
  13. package/dist/gsd/git.d.ts +20 -0
  14. package/dist/gsd/git.d.ts.map +1 -0
  15. package/dist/gsd/git.js +59 -0
  16. package/dist/gsd/git.js.map +1 -0
  17. package/dist/gsd/hook-utils.d.ts +22 -0
  18. package/dist/gsd/hook-utils.d.ts.map +1 -0
  19. package/dist/gsd/hook-utils.js +100 -0
  20. package/dist/gsd/hook-utils.js.map +1 -0
  21. package/dist/gsd/index.d.ts +9 -0
  22. package/dist/gsd/index.d.ts.map +1 -0
  23. package/dist/gsd/index.js +8 -0
  24. package/dist/gsd/index.js.map +1 -0
  25. package/dist/gsd/planning.d.ts +20 -0
  26. package/dist/gsd/planning.d.ts.map +1 -0
  27. package/dist/gsd/planning.js +167 -0
  28. package/dist/gsd/planning.js.map +1 -0
  29. package/dist/hooks/gsd/draht-post-task.js +44 -11
  30. package/dist/hooks/gsd/draht-quality-gate.js +99 -57
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +2 -2
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/prompts/agents/build.md +5 -1
  39. package/dist/prompts/agents/plan.md +5 -1
  40. package/dist/prompts/agents/verify.md +5 -1
  41. package/dist/prompts/commands/atomic-commit.md +8 -16
  42. package/dist/prompts/commands/discuss-phase.md +9 -3
  43. package/dist/prompts/commands/execute-phase.md +15 -8
  44. package/dist/prompts/commands/fix.md +6 -0
  45. package/dist/prompts/commands/init-project.md +9 -3
  46. package/dist/prompts/commands/map-codebase.md +7 -1
  47. package/dist/prompts/commands/new-project.md +8 -2
  48. package/dist/prompts/commands/next-milestone.md +4 -0
  49. package/dist/prompts/commands/pause-work.md +4 -0
  50. package/dist/prompts/commands/plan-phase.md +11 -5
  51. package/dist/prompts/commands/progress.md +4 -0
  52. package/dist/prompts/commands/quick.md +8 -2
  53. package/dist/prompts/commands/resume-work.md +4 -0
  54. package/dist/prompts/commands/review.md +6 -0
  55. package/dist/prompts/commands/verify-work.md +10 -4
  56. package/hooks/gsd/draht-post-task.js +44 -11
  57. package/hooks/gsd/draht-quality-gate.js +99 -57
  58. package/package.json +5 -5
  59. package/prompts/agents/build.md +5 -1
  60. package/prompts/agents/plan.md +5 -1
  61. package/prompts/agents/verify.md +5 -1
  62. package/prompts/commands/atomic-commit.md +8 -16
  63. package/prompts/commands/discuss-phase.md +9 -3
  64. package/prompts/commands/execute-phase.md +15 -8
  65. package/prompts/commands/fix.md +6 -0
  66. package/prompts/commands/init-project.md +9 -3
  67. package/prompts/commands/map-codebase.md +7 -1
  68. package/prompts/commands/new-project.md +8 -2
  69. package/prompts/commands/next-milestone.md +4 -0
  70. package/prompts/commands/pause-work.md +4 -0
  71. package/prompts/commands/plan-phase.md +11 -5
  72. package/prompts/commands/progress.md +4 -0
  73. package/prompts/commands/quick.md +8 -2
  74. package/prompts/commands/resume-work.md +4 -0
  75. package/prompts/commands/review.md +6 -0
  76. package/prompts/commands/verify-work.md +10 -4
@@ -0,0 +1,100 @@
1
+ // GSD Hook Utilities — toolchain auto-detection and hook configuration.
2
+ // Mirrors the inline logic in hooks/gsd/*.js so it can be tested via vitest.
3
+ // The hook .js files embed the same logic inline (with require() fallback to this dist).
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ const DEFAULT_HOOK_CONFIG = {
7
+ coverageThreshold: 80,
8
+ tddMode: "advisory",
9
+ qualityGateStrict: false,
10
+ };
11
+ /**
12
+ * Detect package manager from lockfiles and package.json scripts.
13
+ * Priority: bun.lockb/bun.lock > pnpm-lock.yaml > yarn.lock > package-lock.json > fallback npm
14
+ */
15
+ export function detectToolchain(cwd) {
16
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
17
+ return {
18
+ pm: "bun",
19
+ testCmd: "bun test",
20
+ coverageCmd: "bun test --coverage",
21
+ lintCmd: "bunx biome check .",
22
+ };
23
+ }
24
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
25
+ return {
26
+ pm: "pnpm",
27
+ testCmd: "pnpm test",
28
+ coverageCmd: "pnpm run test:coverage",
29
+ lintCmd: "pnpm run lint",
30
+ };
31
+ }
32
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
33
+ return {
34
+ pm: "yarn",
35
+ testCmd: "yarn test",
36
+ coverageCmd: "yarn run test:coverage",
37
+ lintCmd: "yarn run lint",
38
+ };
39
+ }
40
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
41
+ return {
42
+ pm: "npm",
43
+ testCmd: "npm test",
44
+ coverageCmd: "npm run test:coverage",
45
+ lintCmd: "npm run lint",
46
+ };
47
+ }
48
+ // No lockfile — check package.json scripts for test runner hints
49
+ const pkgPath = path.join(cwd, "package.json");
50
+ if (fs.existsSync(pkgPath)) {
51
+ try {
52
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
53
+ if (pkg.scripts?.test) {
54
+ return {
55
+ pm: "npm",
56
+ testCmd: "npm test",
57
+ coverageCmd: "npm run test:coverage",
58
+ lintCmd: "npm run lint",
59
+ };
60
+ }
61
+ }
62
+ catch {
63
+ /* ignore parse errors */
64
+ }
65
+ }
66
+ // Fallback
67
+ return {
68
+ pm: "npm",
69
+ testCmd: "npm test",
70
+ coverageCmd: "npm run test:coverage",
71
+ lintCmd: "npm run lint",
72
+ };
73
+ }
74
+ /**
75
+ * Read hook configuration from .planning/config.json hooks section.
76
+ * Falls back to defaults on missing file or parse errors.
77
+ */
78
+ export function readHookConfig(cwd) {
79
+ const configPath = path.join(cwd, ".planning", "config.json");
80
+ if (!fs.existsSync(configPath)) {
81
+ return { ...DEFAULT_HOOK_CONFIG };
82
+ }
83
+ try {
84
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
85
+ const hooks = raw.hooks ?? {};
86
+ return {
87
+ coverageThreshold: typeof hooks.coverageThreshold === "number"
88
+ ? hooks.coverageThreshold
89
+ : DEFAULT_HOOK_CONFIG.coverageThreshold,
90
+ tddMode: hooks.tddMode === "strict" || hooks.tddMode === "advisory" ? hooks.tddMode : DEFAULT_HOOK_CONFIG.tddMode,
91
+ qualityGateStrict: typeof hooks.qualityGateStrict === "boolean"
92
+ ? hooks.qualityGateStrict
93
+ : DEFAULT_HOOK_CONFIG.qualityGateStrict,
94
+ };
95
+ }
96
+ catch {
97
+ return { ...DEFAULT_HOOK_CONFIG };
98
+ }
99
+ }
100
+ //# sourceMappingURL=hook-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-utils.js","sourceRoot":"","sources":["../../src/gsd/hook-utils.ts"],"names":[],"mappings":"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","sourcesContent":["// GSD Hook Utilities — toolchain auto-detection and hook configuration.\n// Mirrors the inline logic in hooks/gsd/*.js so it can be tested via vitest.\n// The hook .js files embed the same logic inline (with require() fallback to this dist).\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nexport interface ToolchainInfo {\n\tpm: \"npm\" | \"bun\" | \"pnpm\" | \"yarn\";\n\ttestCmd: string;\n\tcoverageCmd: string;\n\tlintCmd: string;\n}\n\nexport interface HookConfig {\n\tcoverageThreshold: number;\n\ttddMode: \"strict\" | \"advisory\";\n\tqualityGateStrict: boolean;\n}\n\nconst DEFAULT_HOOK_CONFIG: HookConfig = {\n\tcoverageThreshold: 80,\n\ttddMode: \"advisory\",\n\tqualityGateStrict: false,\n};\n\n/**\n * Detect package manager from lockfiles and package.json scripts.\n * Priority: bun.lockb/bun.lock > pnpm-lock.yaml > yarn.lock > package-lock.json > fallback npm\n */\nexport function detectToolchain(cwd: string): ToolchainInfo {\n\tif (fs.existsSync(path.join(cwd, \"bun.lockb\")) || fs.existsSync(path.join(cwd, \"bun.lock\"))) {\n\t\treturn {\n\t\t\tpm: \"bun\",\n\t\t\ttestCmd: \"bun test\",\n\t\t\tcoverageCmd: \"bun test --coverage\",\n\t\t\tlintCmd: \"bunx biome check .\",\n\t\t};\n\t}\n\n\tif (fs.existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) {\n\t\treturn {\n\t\t\tpm: \"pnpm\",\n\t\t\ttestCmd: \"pnpm test\",\n\t\t\tcoverageCmd: \"pnpm run test:coverage\",\n\t\t\tlintCmd: \"pnpm run lint\",\n\t\t};\n\t}\n\n\tif (fs.existsSync(path.join(cwd, \"yarn.lock\"))) {\n\t\treturn {\n\t\t\tpm: \"yarn\",\n\t\t\ttestCmd: \"yarn test\",\n\t\t\tcoverageCmd: \"yarn run test:coverage\",\n\t\t\tlintCmd: \"yarn run lint\",\n\t\t};\n\t}\n\n\tif (fs.existsSync(path.join(cwd, \"package-lock.json\"))) {\n\t\treturn {\n\t\t\tpm: \"npm\",\n\t\t\ttestCmd: \"npm test\",\n\t\t\tcoverageCmd: \"npm run test:coverage\",\n\t\t\tlintCmd: \"npm run lint\",\n\t\t};\n\t}\n\n\t// No lockfile — check package.json scripts for test runner hints\n\tconst pkgPath = path.join(cwd, \"package.json\");\n\tif (fs.existsSync(pkgPath)) {\n\t\ttry {\n\t\t\tconst pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n\t\t\t\tscripts?: Record<string, string>;\n\t\t\t};\n\t\t\tif (pkg.scripts?.test) {\n\t\t\t\treturn {\n\t\t\t\t\tpm: \"npm\",\n\t\t\t\t\ttestCmd: \"npm test\",\n\t\t\t\t\tcoverageCmd: \"npm run test:coverage\",\n\t\t\t\t\tlintCmd: \"npm run lint\",\n\t\t\t\t};\n\t\t\t}\n\t\t} catch {\n\t\t\t/* ignore parse errors */\n\t\t}\n\t}\n\n\t// Fallback\n\treturn {\n\t\tpm: \"npm\",\n\t\ttestCmd: \"npm test\",\n\t\tcoverageCmd: \"npm run test:coverage\",\n\t\tlintCmd: \"npm run lint\",\n\t};\n}\n\n/**\n * Read hook configuration from .planning/config.json hooks section.\n * Falls back to defaults on missing file or parse errors.\n */\nexport function readHookConfig(cwd: string): HookConfig {\n\tconst configPath = path.join(cwd, \".planning\", \"config.json\");\n\tif (!fs.existsSync(configPath)) {\n\t\treturn { ...DEFAULT_HOOK_CONFIG };\n\t}\n\ttry {\n\t\tconst raw = JSON.parse(fs.readFileSync(configPath, \"utf-8\")) as {\n\t\t\thooks?: Partial<HookConfig>;\n\t\t};\n\t\tconst hooks = raw.hooks ?? {};\n\t\treturn {\n\t\t\tcoverageThreshold:\n\t\t\t\ttypeof hooks.coverageThreshold === \"number\"\n\t\t\t\t\t? hooks.coverageThreshold\n\t\t\t\t\t: DEFAULT_HOOK_CONFIG.coverageThreshold,\n\t\t\ttddMode:\n\t\t\t\thooks.tddMode === \"strict\" || hooks.tddMode === \"advisory\" ? hooks.tddMode : DEFAULT_HOOK_CONFIG.tddMode,\n\t\t\tqualityGateStrict:\n\t\t\t\ttypeof hooks.qualityGateStrict === \"boolean\"\n\t\t\t\t\t? hooks.qualityGateStrict\n\t\t\t\t\t: DEFAULT_HOOK_CONFIG.qualityGateStrict,\n\t\t};\n\t} catch {\n\t\treturn { ...DEFAULT_HOOK_CONFIG };\n\t}\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export { createDomainModel, mapCodebase } from "./domain.js";
2
+ export { extractGlossaryTerms, loadDomainContent, validateDomainGlossary, } from "./domain-validator.js";
3
+ export type { CommitResult } from "./git.js";
4
+ export { commitDocs, commitTask, hasTestFiles } from "./git.js";
5
+ export type { HookConfig, ToolchainInfo } from "./hook-utils.js";
6
+ export { detectToolchain, readHookConfig } from "./hook-utils.js";
7
+ export type { PhaseVerification, PlanDiscovery } from "./planning.js";
8
+ export { createPlan, discoverPlans, readPlan, updateState, verifyPhase, writeSummary, } from "./planning.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gsd/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACN,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EACN,UAAU,EACV,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,YAAY,GACZ,MAAM,eAAe,CAAC","sourcesContent":["// GSD index — re-exports all GSD module functions.\n// Import from @draht/coding-agent for use in extensions.\n\nexport { createDomainModel, mapCodebase } from \"./domain.js\";\nexport {\n\textractGlossaryTerms,\n\tloadDomainContent,\n\tvalidateDomainGlossary,\n} from \"./domain-validator.js\";\nexport type { CommitResult } from \"./git.js\";\nexport { commitDocs, commitTask, hasTestFiles } from \"./git.js\";\nexport type { HookConfig, ToolchainInfo } from \"./hook-utils.js\";\nexport { detectToolchain, readHookConfig } from \"./hook-utils.js\";\nexport type { PhaseVerification, PlanDiscovery } from \"./planning.js\";\nexport {\n\tcreatePlan,\n\tdiscoverPlans,\n\treadPlan,\n\tupdateState,\n\tverifyPhase,\n\twriteSummary,\n} from \"./planning.js\";\n"]}
@@ -0,0 +1,8 @@
1
+ // GSD index — re-exports all GSD module functions.
2
+ // Import from @draht/coding-agent for use in extensions.
3
+ export { createDomainModel, mapCodebase } from "./domain.js";
4
+ export { extractGlossaryTerms, loadDomainContent, validateDomainGlossary, } from "./domain-validator.js";
5
+ export { commitDocs, commitTask, hasTestFiles } from "./git.js";
6
+ export { detectToolchain, readHookConfig } from "./hook-utils.js";
7
+ export { createPlan, discoverPlans, readPlan, updateState, verifyPhase, writeSummary, } from "./planning.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gsd/index.ts"],"names":[],"mappings":"AAAA,qDAAmD;AACnD,yDAAyD;AAEzD,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACN,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEhE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAElE,OAAO,EACN,UAAU,EACV,aAAa,EACb,QAAQ,EACR,WAAW,EACX,WAAW,EACX,YAAY,GACZ,MAAM,eAAe,CAAC","sourcesContent":["// GSD index — re-exports all GSD module functions.\n// Import from @draht/coding-agent for use in extensions.\n\nexport { createDomainModel, mapCodebase } from \"./domain.js\";\nexport {\n\textractGlossaryTerms,\n\tloadDomainContent,\n\tvalidateDomainGlossary,\n} from \"./domain-validator.js\";\nexport type { CommitResult } from \"./git.js\";\nexport { commitDocs, commitTask, hasTestFiles } from \"./git.js\";\nexport type { HookConfig, ToolchainInfo } from \"./hook-utils.js\";\nexport { detectToolchain, readHookConfig } from \"./hook-utils.js\";\nexport type { PhaseVerification, PlanDiscovery } from \"./planning.js\";\nexport {\n\tcreatePlan,\n\tdiscoverPlans,\n\treadPlan,\n\tupdateState,\n\tverifyPhase,\n\twriteSummary,\n} from \"./planning.js\";\n"]}
@@ -0,0 +1,20 @@
1
+ export interface PlanDiscovery {
2
+ plans: Array<{
3
+ file: string;
4
+ deps: string[];
5
+ }>;
6
+ incomplete: string[];
7
+ fixPlans: string[];
8
+ }
9
+ export interface PhaseVerification {
10
+ plans: number;
11
+ summaries: number;
12
+ complete: boolean;
13
+ }
14
+ export declare function createPlan(cwd: string, phaseNum: number, planNum: number, title?: string): string;
15
+ export declare function discoverPlans(cwd: string, phaseNum: number): PlanDiscovery;
16
+ export declare function readPlan(cwd: string, phaseNum: number, planNum: number): string;
17
+ export declare function writeSummary(cwd: string, phaseNum: number, planNum: number): string;
18
+ export declare function verifyPhase(cwd: string, phaseNum: number): PhaseVerification;
19
+ export declare function updateState(cwd: string): void;
20
+ //# sourceMappingURL=planning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planning.d.ts","sourceRoot":"","sources":["../../src/gsd/planning.ts"],"names":[],"mappings":"AA8CA,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAyCjG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CA+B1E;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM/E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBnF;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAe5E;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAM7C","sourcesContent":["// GSD Planning module — phase/plan/task file system operations.\n// Part of the draht GSD (Get Shit Done) methodology.\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nconst PLANNING = \".planning\";\n\nfunction planningPath(cwd: string, ...segments: string[]): string {\n\treturn path.join(cwd, PLANNING, ...segments);\n}\n\nfunction ensureDir(dir: string): void {\n\tif (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n}\n\nfunction padNum(n: number, digits = 2): string {\n\treturn String(n).padStart(digits, \"0\");\n}\n\nfunction timestamp(): string {\n\treturn new Date().toISOString().replace(\"T\", \" \").slice(0, 19);\n}\n\nfunction getPhaseSlug(cwd: string, phaseNum: number): string {\n\tconst roadmapPath = planningPath(cwd, \"ROADMAP.md\");\n\tif (!fs.existsSync(roadmapPath)) return `phase-${phaseNum}`;\n\tconst content = fs.readFileSync(roadmapPath, \"utf-8\");\n\tconst re = new RegExp(`## Phase ${phaseNum}: (.+?) —`);\n\tconst m = re.exec(content);\n\tif (!m) return `phase-${phaseNum}`;\n\treturn m[1]\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\")\n\t\t.slice(0, 40);\n}\n\nfunction getPhaseDir(cwd: string, phaseNum: number): string | null {\n\tconst phasesDir = planningPath(cwd, \"phases\");\n\tif (!fs.existsSync(phasesDir)) return null;\n\tconst prefix = `${padNum(phaseNum)}-`;\n\tconst entry = fs.readdirSync(phasesDir).find((e) => e.startsWith(prefix));\n\treturn entry ? path.join(phasesDir, entry) : null;\n}\n\nexport interface PlanDiscovery {\n\tplans: Array<{ file: string; deps: string[] }>;\n\tincomplete: string[];\n\tfixPlans: string[];\n}\n\nexport interface PhaseVerification {\n\tplans: number;\n\tsummaries: number;\n\tcomplete: boolean;\n}\n\nexport function createPlan(cwd: string, phaseNum: number, planNum: number, title?: string): string {\n\tconst slug = getPhaseSlug(cwd, phaseNum);\n\tconst dir = planningPath(cwd, \"phases\", `${padNum(phaseNum)}-${slug}`);\n\tensureDir(dir);\n\tconst planTitle = title || `Plan ${planNum}`;\n\tconst planFile = path.join(dir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);\n\tconst tmpl = `---\nphase: ${phaseNum}\nplan: ${planNum}\ndepends_on: []\nmust_haves:\n - \"[Observable truth this plan delivers]\"\n---\n\n# Phase ${phaseNum}, Plan ${planNum}: ${planTitle}\n\n## Goal\n[What this plan achieves from user perspective]\n\n## Context\n[Key decisions that affect this plan]\n\n## Tasks\n\n<task type=\"auto\">\n <n>[Task name]</n>\n <files>[affected files]</files>\n <test>[Write tests first — what should pass when done]</test>\n <action>\n [Implementation to make tests pass]\n </action>\n <refactor>[Optional cleanup after green]</refactor>\n <verify>[How to verify]</verify>\n <done>[What \"done\" looks like]</done>\n</task>\n\n---\nCreated: ${timestamp()}\n`;\n\tfs.writeFileSync(planFile, tmpl, \"utf-8\");\n\treturn planFile;\n}\n\nexport function discoverPlans(cwd: string, phaseNum: number): PlanDiscovery {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} directory not found`);\n\n\tconst files = fs.readdirSync(phaseDir).sort();\n\tconst plans = files.filter((f) => f.endsWith(\"-PLAN.md\") && !f.includes(\"FIX\"));\n\tconst summaries = files.filter((f) => f.endsWith(\"-SUMMARY.md\"));\n\tconst fixPlans = files.filter((f) => f.includes(\"FIX-PLAN.md\"));\n\n\tconst completedPlanNums = new Set(\n\t\tsummaries.map((s) => s.match(/\\d+-(\\d+)-SUMMARY/)?.[1]).filter((x): x is string => Boolean(x)),\n\t);\n\n\tconst incomplete = plans.filter((p) => {\n\t\tconst m = p.match(/\\d+-(\\d+)-PLAN/);\n\t\treturn m ? !completedPlanNums.has(m[1]) : true;\n\t});\n\n\tconst planData = plans.map((p) => {\n\t\tconst content = fs.readFileSync(path.join(phaseDir, p), \"utf-8\");\n\t\tconst depsMatch = content.match(/depends_on:\\s*\\[(.*?)\\]/);\n\t\tconst deps = depsMatch\n\t\t\t? depsMatch[1]\n\t\t\t\t\t.split(\",\")\n\t\t\t\t\t.map((d) => d.trim())\n\t\t\t\t\t.filter(Boolean)\n\t\t\t: [];\n\t\treturn { file: p, deps };\n\t});\n\n\treturn { plans: planData, incomplete, fixPlans };\n}\n\nexport function readPlan(cwd: string, phaseNum: number, planNum: number): string {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst planFile = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);\n\tif (!fs.existsSync(planFile)) throw new Error(`Plan file not found: ${planFile}`);\n\treturn fs.readFileSync(planFile, \"utf-8\");\n}\n\nexport function writeSummary(cwd: string, phaseNum: number, planNum: number): string {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst summaryPath = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-SUMMARY.md`);\n\tconst tmpl = `# 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`;\n\tfs.writeFileSync(summaryPath, tmpl, \"utf-8\");\n\treturn summaryPath;\n}\n\nexport function verifyPhase(cwd: string, phaseNum: number): PhaseVerification {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith(\"-PLAN.md\") && !f.includes(\"FIX\"));\n\tconst summaries = fs.readdirSync(phaseDir).filter((f) => f.endsWith(\"-SUMMARY.md\"));\n\tconst complete = summaries.length >= plans.length && plans.length > 0;\n\tif (complete) {\n\t\tconst verPath = path.join(phaseDir, `${padNum(phaseNum)}-VERIFICATION.md`);\n\t\tfs.writeFileSync(\n\t\t\tverPath,\n\t\t\t`# Phase ${phaseNum} Verification\\n\\nAll ${plans.length} plans executed.\\nVerified: ${timestamp()}\\n`,\n\t\t\t\"utf-8\",\n\t\t);\n\t}\n\treturn { plans: plans.length, summaries: summaries.length, complete };\n}\n\nexport function updateState(cwd: string): void {\n\tconst statePath = planningPath(cwd, \"STATE.md\");\n\tif (!fs.existsSync(statePath)) throw new Error(\"No STATE.md found\");\n\tlet state = fs.readFileSync(statePath, \"utf-8\");\n\tstate = state.replace(/## Last Activity:.*/, `## Last Activity: ${timestamp()}`);\n\tfs.writeFileSync(statePath, state, \"utf-8\");\n}\n"]}
@@ -0,0 +1,167 @@
1
+ // GSD Planning module — phase/plan/task file system operations.
2
+ // Part of the draht GSD (Get Shit Done) methodology.
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ const PLANNING = ".planning";
6
+ function planningPath(cwd, ...segments) {
7
+ return path.join(cwd, PLANNING, ...segments);
8
+ }
9
+ function ensureDir(dir) {
10
+ if (!fs.existsSync(dir))
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ }
13
+ function padNum(n, digits = 2) {
14
+ return String(n).padStart(digits, "0");
15
+ }
16
+ function timestamp() {
17
+ return new Date().toISOString().replace("T", " ").slice(0, 19);
18
+ }
19
+ function getPhaseSlug(cwd, phaseNum) {
20
+ const roadmapPath = planningPath(cwd, "ROADMAP.md");
21
+ if (!fs.existsSync(roadmapPath))
22
+ return `phase-${phaseNum}`;
23
+ const content = fs.readFileSync(roadmapPath, "utf-8");
24
+ const re = new RegExp(`## Phase ${phaseNum}: (.+?) —`);
25
+ const m = re.exec(content);
26
+ if (!m)
27
+ return `phase-${phaseNum}`;
28
+ return m[1]
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]+/g, "-")
31
+ .replace(/^-|-$/g, "")
32
+ .slice(0, 40);
33
+ }
34
+ function getPhaseDir(cwd, phaseNum) {
35
+ const phasesDir = planningPath(cwd, "phases");
36
+ if (!fs.existsSync(phasesDir))
37
+ return null;
38
+ const prefix = `${padNum(phaseNum)}-`;
39
+ const entry = fs.readdirSync(phasesDir).find((e) => e.startsWith(prefix));
40
+ return entry ? path.join(phasesDir, entry) : null;
41
+ }
42
+ export function createPlan(cwd, phaseNum, planNum, title) {
43
+ const slug = getPhaseSlug(cwd, phaseNum);
44
+ const dir = planningPath(cwd, "phases", `${padNum(phaseNum)}-${slug}`);
45
+ ensureDir(dir);
46
+ const planTitle = title || `Plan ${planNum}`;
47
+ const planFile = path.join(dir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);
48
+ const tmpl = `---
49
+ phase: ${phaseNum}
50
+ plan: ${planNum}
51
+ depends_on: []
52
+ must_haves:
53
+ - "[Observable truth this plan delivers]"
54
+ ---
55
+
56
+ # Phase ${phaseNum}, Plan ${planNum}: ${planTitle}
57
+
58
+ ## Goal
59
+ [What this plan achieves from user perspective]
60
+
61
+ ## Context
62
+ [Key decisions that affect this plan]
63
+
64
+ ## Tasks
65
+
66
+ <task type="auto">
67
+ <n>[Task name]</n>
68
+ <files>[affected files]</files>
69
+ <test>[Write tests first — what should pass when done]</test>
70
+ <action>
71
+ [Implementation to make tests pass]
72
+ </action>
73
+ <refactor>[Optional cleanup after green]</refactor>
74
+ <verify>[How to verify]</verify>
75
+ <done>[What "done" looks like]</done>
76
+ </task>
77
+
78
+ ---
79
+ Created: ${timestamp()}
80
+ `;
81
+ fs.writeFileSync(planFile, tmpl, "utf-8");
82
+ return planFile;
83
+ }
84
+ export function discoverPlans(cwd, phaseNum) {
85
+ const phaseDir = getPhaseDir(cwd, phaseNum);
86
+ if (!phaseDir)
87
+ throw new Error(`Phase ${phaseNum} directory not found`);
88
+ const files = fs.readdirSync(phaseDir).sort();
89
+ const plans = files.filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
90
+ const summaries = files.filter((f) => f.endsWith("-SUMMARY.md"));
91
+ const fixPlans = files.filter((f) => f.includes("FIX-PLAN.md"));
92
+ const completedPlanNums = new Set(summaries.map((s) => s.match(/\d+-(\d+)-SUMMARY/)?.[1]).filter((x) => Boolean(x)));
93
+ const incomplete = plans.filter((p) => {
94
+ const m = p.match(/\d+-(\d+)-PLAN/);
95
+ return m ? !completedPlanNums.has(m[1]) : true;
96
+ });
97
+ const planData = plans.map((p) => {
98
+ const content = fs.readFileSync(path.join(phaseDir, p), "utf-8");
99
+ const depsMatch = content.match(/depends_on:\s*\[(.*?)\]/);
100
+ const deps = depsMatch
101
+ ? depsMatch[1]
102
+ .split(",")
103
+ .map((d) => d.trim())
104
+ .filter(Boolean)
105
+ : [];
106
+ return { file: p, deps };
107
+ });
108
+ return { plans: planData, incomplete, fixPlans };
109
+ }
110
+ export function readPlan(cwd, phaseNum, planNum) {
111
+ const phaseDir = getPhaseDir(cwd, phaseNum);
112
+ if (!phaseDir)
113
+ throw new Error(`Phase ${phaseNum} not found`);
114
+ const planFile = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);
115
+ if (!fs.existsSync(planFile))
116
+ throw new Error(`Plan file not found: ${planFile}`);
117
+ return fs.readFileSync(planFile, "utf-8");
118
+ }
119
+ export function writeSummary(cwd, phaseNum, planNum) {
120
+ const phaseDir = getPhaseDir(cwd, phaseNum);
121
+ if (!phaseDir)
122
+ throw new Error(`Phase ${phaseNum} not found`);
123
+ const summaryPath = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-SUMMARY.md`);
124
+ const tmpl = `# Phase ${phaseNum}, Plan ${planNum} Summary
125
+
126
+ ## Completed Tasks
127
+ | # | Task | Status | Commit |
128
+ |---|------|--------|--------|
129
+ | 1 | [task] | ✅ Done | [hash] |
130
+
131
+ ## Files Changed
132
+ - [files]
133
+
134
+ ## Verification Results
135
+ - [results]
136
+
137
+ ## Notes
138
+ [deviations, decisions]
139
+
140
+ ---
141
+ Completed: ${timestamp()}
142
+ `;
143
+ fs.writeFileSync(summaryPath, tmpl, "utf-8");
144
+ return summaryPath;
145
+ }
146
+ export function verifyPhase(cwd, phaseNum) {
147
+ const phaseDir = getPhaseDir(cwd, phaseNum);
148
+ if (!phaseDir)
149
+ throw new Error(`Phase ${phaseNum} not found`);
150
+ const plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-PLAN.md") && !f.includes("FIX"));
151
+ const summaries = fs.readdirSync(phaseDir).filter((f) => f.endsWith("-SUMMARY.md"));
152
+ const complete = summaries.length >= plans.length && plans.length > 0;
153
+ if (complete) {
154
+ const verPath = path.join(phaseDir, `${padNum(phaseNum)}-VERIFICATION.md`);
155
+ fs.writeFileSync(verPath, `# Phase ${phaseNum} Verification\n\nAll ${plans.length} plans executed.\nVerified: ${timestamp()}\n`, "utf-8");
156
+ }
157
+ return { plans: plans.length, summaries: summaries.length, complete };
158
+ }
159
+ export function updateState(cwd) {
160
+ const statePath = planningPath(cwd, "STATE.md");
161
+ if (!fs.existsSync(statePath))
162
+ throw new Error("No STATE.md found");
163
+ let state = fs.readFileSync(statePath, "utf-8");
164
+ state = state.replace(/## Last Activity:.*/, `## Last Activity: ${timestamp()}`);
165
+ fs.writeFileSync(statePath, state, "utf-8");
166
+ }
167
+ //# sourceMappingURL=planning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planning.js","sourceRoot":"","sources":["../../src/gsd/planning.ts"],"names":[],"mappings":"AAAA,kEAAgE;AAChE,qDAAqD;AAErD,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B,SAAS,YAAY,CAAC,GAAW,EAAE,GAAG,QAAkB,EAAU;IACjE,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAC;AAAA,CAC7C;AAED,SAAS,SAAS,CAAC,GAAW,EAAQ;IACrC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAAA,CAChE;AAED,SAAS,MAAM,CAAC,CAAS,EAAE,MAAM,GAAG,CAAC,EAAU;IAC9C,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAAA,CACvC;AAED,SAAS,SAAS,GAAW;IAC5B,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CAC/D;AAED,SAAS,YAAY,CAAC,GAAW,EAAE,QAAgB,EAAU;IAC5D,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,SAAS,QAAQ,EAAE,CAAC;IAC5D,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,YAAY,QAAQ,aAAW,CAAC,CAAC;IACvD,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,QAAQ,EAAE,CAAC;IACnC,OAAO,CAAC,CAAC,CAAC,CAAC;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAAA,CACf;AAED,SAAS,WAAW,CAAC,GAAW,EAAE,QAAgB,EAAiB;IAClE,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;IACtC,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1E,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAAA,CAClD;AAcD,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAe,EAAE,KAAc,EAAU;IAClG,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACvE,SAAS,CAAC,GAAG,CAAC,CAAC;IACf,MAAM,SAAS,GAAG,KAAK,IAAI,QAAQ,OAAO,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG;SACL,QAAQ;QACT,OAAO;;;;;;UAML,QAAQ,UAAU,OAAO,KAAK,SAAS;;;;;;;;;;;;;;;;;;;;;;;WAuBtC,SAAS,EAAE;CACrB,CAAC;IACD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,QAAgB,EAAiB;IAC3E,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,sBAAsB,CAAC,CAAC;IAExE,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAChF,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IAEhE,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAChC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAC9F,CAAC;IAEF,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACpC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAAA,CAC/C,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACjE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,SAAS;YACrB,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;iBACX,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,OAAO,CAAC;YAClB,CAAC,CAAC,EAAE,CAAC;QACN,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IAAA,CACzB,CAAC,CAAC;IAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAAA,CACjD;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAe,EAAU;IAChF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,YAAY,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;IAClF,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAAA,CAC1C;AAED,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAe,EAAU;IACpF,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,YAAY,CAAC,CAAC;IAC9D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7F,MAAM,IAAI,GAAG,WAAW,QAAQ,UAAU,OAAO;;;;;;;;;;;;;;;;;aAiBrC,SAAS,EAAE;CACvB,CAAC;IACD,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7C,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAgB,EAAqB;IAC7E,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,SAAS,QAAQ,YAAY,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACnG,MAAM,SAAS,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IACpF,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IACtE,IAAI,QAAQ,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QAC3E,EAAE,CAAC,aAAa,CACf,OAAO,EACP,WAAW,QAAQ,wBAAwB,KAAK,CAAC,MAAM,+BAA+B,SAAS,EAAE,IAAI,EACrG,OAAO,CACP,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;AAAA,CACtE;AAED,MAAM,UAAU,WAAW,CAAC,GAAW,EAAQ;IAC9C,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAChD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACpE,IAAI,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAChD,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,qBAAqB,SAAS,EAAE,EAAE,CAAC,CAAC;IACjF,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAAA,CAC5C","sourcesContent":["// GSD Planning module — phase/plan/task file system operations.\n// Part of the draht GSD (Get Shit Done) methodology.\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nconst PLANNING = \".planning\";\n\nfunction planningPath(cwd: string, ...segments: string[]): string {\n\treturn path.join(cwd, PLANNING, ...segments);\n}\n\nfunction ensureDir(dir: string): void {\n\tif (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n}\n\nfunction padNum(n: number, digits = 2): string {\n\treturn String(n).padStart(digits, \"0\");\n}\n\nfunction timestamp(): string {\n\treturn new Date().toISOString().replace(\"T\", \" \").slice(0, 19);\n}\n\nfunction getPhaseSlug(cwd: string, phaseNum: number): string {\n\tconst roadmapPath = planningPath(cwd, \"ROADMAP.md\");\n\tif (!fs.existsSync(roadmapPath)) return `phase-${phaseNum}`;\n\tconst content = fs.readFileSync(roadmapPath, \"utf-8\");\n\tconst re = new RegExp(`## Phase ${phaseNum}: (.+?) —`);\n\tconst m = re.exec(content);\n\tif (!m) return `phase-${phaseNum}`;\n\treturn m[1]\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t.replace(/^-|-$/g, \"\")\n\t\t.slice(0, 40);\n}\n\nfunction getPhaseDir(cwd: string, phaseNum: number): string | null {\n\tconst phasesDir = planningPath(cwd, \"phases\");\n\tif (!fs.existsSync(phasesDir)) return null;\n\tconst prefix = `${padNum(phaseNum)}-`;\n\tconst entry = fs.readdirSync(phasesDir).find((e) => e.startsWith(prefix));\n\treturn entry ? path.join(phasesDir, entry) : null;\n}\n\nexport interface PlanDiscovery {\n\tplans: Array<{ file: string; deps: string[] }>;\n\tincomplete: string[];\n\tfixPlans: string[];\n}\n\nexport interface PhaseVerification {\n\tplans: number;\n\tsummaries: number;\n\tcomplete: boolean;\n}\n\nexport function createPlan(cwd: string, phaseNum: number, planNum: number, title?: string): string {\n\tconst slug = getPhaseSlug(cwd, phaseNum);\n\tconst dir = planningPath(cwd, \"phases\", `${padNum(phaseNum)}-${slug}`);\n\tensureDir(dir);\n\tconst planTitle = title || `Plan ${planNum}`;\n\tconst planFile = path.join(dir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);\n\tconst tmpl = `---\nphase: ${phaseNum}\nplan: ${planNum}\ndepends_on: []\nmust_haves:\n - \"[Observable truth this plan delivers]\"\n---\n\n# Phase ${phaseNum}, Plan ${planNum}: ${planTitle}\n\n## Goal\n[What this plan achieves from user perspective]\n\n## Context\n[Key decisions that affect this plan]\n\n## Tasks\n\n<task type=\"auto\">\n <n>[Task name]</n>\n <files>[affected files]</files>\n <test>[Write tests first — what should pass when done]</test>\n <action>\n [Implementation to make tests pass]\n </action>\n <refactor>[Optional cleanup after green]</refactor>\n <verify>[How to verify]</verify>\n <done>[What \"done\" looks like]</done>\n</task>\n\n---\nCreated: ${timestamp()}\n`;\n\tfs.writeFileSync(planFile, tmpl, \"utf-8\");\n\treturn planFile;\n}\n\nexport function discoverPlans(cwd: string, phaseNum: number): PlanDiscovery {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} directory not found`);\n\n\tconst files = fs.readdirSync(phaseDir).sort();\n\tconst plans = files.filter((f) => f.endsWith(\"-PLAN.md\") && !f.includes(\"FIX\"));\n\tconst summaries = files.filter((f) => f.endsWith(\"-SUMMARY.md\"));\n\tconst fixPlans = files.filter((f) => f.includes(\"FIX-PLAN.md\"));\n\n\tconst completedPlanNums = new Set(\n\t\tsummaries.map((s) => s.match(/\\d+-(\\d+)-SUMMARY/)?.[1]).filter((x): x is string => Boolean(x)),\n\t);\n\n\tconst incomplete = plans.filter((p) => {\n\t\tconst m = p.match(/\\d+-(\\d+)-PLAN/);\n\t\treturn m ? !completedPlanNums.has(m[1]) : true;\n\t});\n\n\tconst planData = plans.map((p) => {\n\t\tconst content = fs.readFileSync(path.join(phaseDir, p), \"utf-8\");\n\t\tconst depsMatch = content.match(/depends_on:\\s*\\[(.*?)\\]/);\n\t\tconst deps = depsMatch\n\t\t\t? depsMatch[1]\n\t\t\t\t\t.split(\",\")\n\t\t\t\t\t.map((d) => d.trim())\n\t\t\t\t\t.filter(Boolean)\n\t\t\t: [];\n\t\treturn { file: p, deps };\n\t});\n\n\treturn { plans: planData, incomplete, fixPlans };\n}\n\nexport function readPlan(cwd: string, phaseNum: number, planNum: number): string {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst planFile = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-PLAN.md`);\n\tif (!fs.existsSync(planFile)) throw new Error(`Plan file not found: ${planFile}`);\n\treturn fs.readFileSync(planFile, \"utf-8\");\n}\n\nexport function writeSummary(cwd: string, phaseNum: number, planNum: number): string {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst summaryPath = path.join(phaseDir, `${padNum(phaseNum)}-${padNum(planNum)}-SUMMARY.md`);\n\tconst tmpl = `# 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`;\n\tfs.writeFileSync(summaryPath, tmpl, \"utf-8\");\n\treturn summaryPath;\n}\n\nexport function verifyPhase(cwd: string, phaseNum: number): PhaseVerification {\n\tconst phaseDir = getPhaseDir(cwd, phaseNum);\n\tif (!phaseDir) throw new Error(`Phase ${phaseNum} not found`);\n\tconst plans = fs.readdirSync(phaseDir).filter((f) => f.endsWith(\"-PLAN.md\") && !f.includes(\"FIX\"));\n\tconst summaries = fs.readdirSync(phaseDir).filter((f) => f.endsWith(\"-SUMMARY.md\"));\n\tconst complete = summaries.length >= plans.length && plans.length > 0;\n\tif (complete) {\n\t\tconst verPath = path.join(phaseDir, `${padNum(phaseNum)}-VERIFICATION.md`);\n\t\tfs.writeFileSync(\n\t\t\tverPath,\n\t\t\t`# Phase ${phaseNum} Verification\\n\\nAll ${plans.length} plans executed.\\nVerified: ${timestamp()}\\n`,\n\t\t\t\"utf-8\",\n\t\t);\n\t}\n\treturn { plans: plans.length, summaries: summaries.length, complete };\n}\n\nexport function updateState(cwd: string): void {\n\tconst statePath = planningPath(cwd, \"STATE.md\");\n\tif (!fs.existsSync(statePath)) throw new Error(\"No STATE.md found\");\n\tlet state = fs.readFileSync(statePath, \"utf-8\");\n\tstate = state.replace(/## Last Activity:.*/, `## Last Activity: ${timestamp()}`);\n\tfs.writeFileSync(statePath, state, \"utf-8\");\n}\n"]}
@@ -19,28 +19,60 @@ if (!phaseNum || !planNum || !taskNum || !status) {
19
19
  process.exit(1);
20
20
  }
21
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
+
22
51
  const PLANNING = ".planning";
23
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);
24
56
 
25
57
  // 0. TDD cycle compliance check
26
- // If the current commit message starts with "green:", the previous commit for this
27
- // task should start with "red:" — enforce the Red → Green order.
28
58
  if (commitHash) {
29
59
  try {
30
- // Find the commit message for commitHash and the one before it
31
60
  const currentMsg = execSync(`git log --format=%s -n 1 ${commitHash} 2>/dev/null`, { encoding: "utf-8" }).trim();
32
61
  if (/^green:/i.test(currentMsg)) {
33
- // Scope search to commits that mention this phase/plan/task in their message
34
- // to avoid false positives from unrelated older commits
35
62
  const taskPrefix = `${phaseNum}-${planNum}-${taskNum}`;
36
63
  const recentMsgs = execSync(`git log --format=%s -n 50 ${commitHash}~1 2>/dev/null`, { encoding: "utf-8" })
37
64
  .trim()
38
65
  .split("\n")
39
66
  .filter((m) => m.includes(taskPrefix) || /^(red|green|refactor):/i.test(m));
40
- // Find the nearest TDD-cycle commit scoped to this task
41
67
  const prevTaskMsg = recentMsgs.find((m) => /^(red|green|refactor):/i.test(m) && m.includes(taskPrefix));
42
68
  if (!prevTaskMsg || !/^red:/i.test(prevTaskMsg)) {
43
- console.log(`⚠️ TDD violation: "green:" commit detected but no preceding "red:" commit found for task ${phaseNum}-${planNum}-${taskNum}`);
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
+ }
44
76
  fs.appendFileSync(LOG_FILE, JSON.stringify({
45
77
  timestamp: new Date().toISOString(),
46
78
  phase: parseInt(phaseNum, 10),
@@ -69,13 +101,15 @@ const entry = {
69
101
 
70
102
  fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
71
103
 
72
- // 2. Run type check if status is pass
104
+ // 2. Run type check and tests if status is pass
73
105
  if (status === "pass") {
106
+ // Type check
74
107
  try {
75
- execSync("bun run tsgo --noEmit 2>&1", { timeout: 30000, encoding: "utf-8" });
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 });
76
110
  // Run tests
77
111
  try {
78
- const testOutput = execSync("bun test 2>&1", { timeout: 60000, encoding: "utf-8" });
112
+ const testOutput = execSync(`${toolchain.testCmd} 2>&1`, { timeout: 60000, encoding: "utf-8", cwd });
79
113
  const testMatch = testOutput.match(/(\d+) pass/);
80
114
  const testCount = testMatch ? testMatch[1] : "?";
81
115
  console.log(`✅ Task ${phaseNum}-${planNum}-${taskNum}: passed + types clean + ${testCount} tests pass`);
@@ -93,7 +127,6 @@ if (status === "pass") {
93
127
  const errorCount = (output.match(/error TS/g) || []).length;
94
128
  if (errorCount > 0) {
95
129
  console.log(`⚠️ Task ${phaseNum}-${planNum}-${taskNum}: passed but ${errorCount} type error(s) introduced`);
96
- // Append warning to log
97
130
  fs.appendFileSync(LOG_FILE, JSON.stringify({
98
131
  ...entry,
99
132
  warning: `${errorCount} type errors introduced`,