@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.
- package/CHANGELOG.md +24 -0
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +4 -2
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/gsd/domain-validator.d.ts +18 -0
- package/dist/gsd/domain-validator.d.ts.map +1 -0
- package/dist/gsd/domain-validator.js +61 -0
- package/dist/gsd/domain-validator.js.map +1 -0
- package/dist/gsd/domain.d.ts +12 -0
- package/dist/gsd/domain.d.ts.map +1 -0
- package/dist/gsd/domain.js +113 -0
- package/dist/gsd/domain.js.map +1 -0
- package/dist/gsd/git.d.ts +20 -0
- package/dist/gsd/git.d.ts.map +1 -0
- package/dist/gsd/git.js +59 -0
- package/dist/gsd/git.js.map +1 -0
- package/dist/gsd/hook-utils.d.ts +22 -0
- package/dist/gsd/hook-utils.d.ts.map +1 -0
- package/dist/gsd/hook-utils.js +100 -0
- package/dist/gsd/hook-utils.js.map +1 -0
- package/dist/gsd/index.d.ts +9 -0
- package/dist/gsd/index.d.ts.map +1 -0
- package/dist/gsd/index.js +8 -0
- package/dist/gsd/index.js.map +1 -0
- package/dist/gsd/planning.d.ts +20 -0
- package/dist/gsd/planning.d.ts.map +1 -0
- package/dist/gsd/planning.js +167 -0
- package/dist/gsd/planning.js.map +1 -0
- package/dist/hooks/gsd/draht-post-task.js +44 -11
- package/dist/hooks/gsd/draht-quality-gate.js +99 -57
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/prompts/agents/build.md +5 -1
- package/dist/prompts/agents/plan.md +5 -1
- package/dist/prompts/agents/verify.md +5 -1
- package/dist/prompts/commands/atomic-commit.md +8 -16
- package/dist/prompts/commands/discuss-phase.md +9 -3
- package/dist/prompts/commands/execute-phase.md +15 -8
- package/dist/prompts/commands/fix.md +6 -0
- package/dist/prompts/commands/init-project.md +9 -3
- package/dist/prompts/commands/map-codebase.md +7 -1
- package/dist/prompts/commands/new-project.md +8 -2
- package/dist/prompts/commands/next-milestone.md +4 -0
- package/dist/prompts/commands/pause-work.md +4 -0
- package/dist/prompts/commands/plan-phase.md +11 -5
- package/dist/prompts/commands/progress.md +4 -0
- package/dist/prompts/commands/quick.md +8 -2
- package/dist/prompts/commands/resume-work.md +4 -0
- package/dist/prompts/commands/review.md +6 -0
- package/dist/prompts/commands/verify-work.md +10 -4
- package/hooks/gsd/draht-post-task.js +44 -11
- package/hooks/gsd/draht-quality-gate.js +99 -57
- package/package.json +5 -5
- package/prompts/agents/build.md +5 -1
- package/prompts/agents/plan.md +5 -1
- package/prompts/agents/verify.md +5 -1
- package/prompts/commands/atomic-commit.md +8 -16
- package/prompts/commands/discuss-phase.md +9 -3
- package/prompts/commands/execute-phase.md +15 -8
- package/prompts/commands/fix.md +6 -0
- package/prompts/commands/init-project.md +9 -3
- package/prompts/commands/map-codebase.md +7 -1
- package/prompts/commands/new-project.md +8 -2
- package/prompts/commands/next-milestone.md +4 -0
- package/prompts/commands/pause-work.md +4 -0
- package/prompts/commands/plan-phase.md +11 -5
- package/prompts/commands/progress.md +4 -0
- package/prompts/commands/quick.md +8 -2
- package/prompts/commands/resume-work.md +4 -0
- package/prompts/commands/review.md +6 -0
- 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":"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","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
|
-
|
|
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
|
-
|
|
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(
|
|
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`,
|