@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2026.3.5] - 2026-03-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- green: domain-validator exported from gsd/index, quality gate uses DOMAIN-MODEL.md
|
|
8
|
+
- green: domain glossary extraction and validation with vitest tests
|
|
9
|
+
- green: toolchain auto-detection and configurable hook thresholds
|
|
10
|
+
- green: detectToolchain and readHookConfig with vitest tests
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- update terminal title icon from pi to D
|
|
15
|
+
- test gsd-commands extension registers create-plan, commit-task, create-domain-model, map-codebase
|
|
16
|
+
- add gsd-index test file
|
|
17
|
+
- create gsd/index.ts and wire GSD exports into @draht/coding-agent
|
|
18
|
+
- implement gsd git module — hasTestFiles, commitTask, commitDocs
|
|
19
|
+
- test gsd git module — hasTestFiles, commitTask, commitDocs
|
|
20
|
+
- implement gsd domain module — createDomainModel, mapCodebase
|
|
21
|
+
- test gsd domain module — createDomainModel, mapCodebase
|
|
22
|
+
- implement gsd planning module — createPlan, discoverPlans, readPlan, writeSummary, verifyPhase, updateState
|
|
23
|
+
- test gsd planning module — createPlan, discoverPlans, readPlan, writeSummary, verifyPhase, updateState
|
|
24
|
+
- add frontmatter metadata and parameter placeholders to prompt templates
|
|
25
|
+
- update author field across all packages
|
|
26
|
+
|
|
3
27
|
## [2026.3.4] - 2026-03-04
|
|
4
28
|
|
|
5
29
|
### Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-templates.d.ts","sourceRoot":"","sources":["../../src/core/prompt-templates.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAkCtE;AA6ED,MAAM,WAAW,0BAA0B;IAC1C,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAoBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,0BAA+B,GAAG,cAAc,EAAE,CAoF9F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,MAAM,CActF","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, isAbsolute, join, resolve, sep } from \"path\";\nimport { CONFIG_DIR_NAME, getPromptsDir, getShippedPromptsDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\n/**\n * Represents a prompt template loaded from a markdown file\n */\nexport interface PromptTemplate {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // \"user\", \"project\", or \"path\"\n\tfilePath: string; // Absolute path to the template file\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in template content\n * Supports:\n * - $1, $2, ... for positional args\n * - $@ and $ARGUMENTS for all args\n * - ${@:N} for args from Nth onwards (bash-style slicing)\n * - ${@:N:L} for L args starting from Nth\n *\n * Note: Replacement happens on the template string only. Argument values\n * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $1, $2, etc. with positional args FIRST (before wildcards)\n\t// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\t// Replace ${@:start} or ${@:start:length} with sliced args (bash-style)\n\t// Process BEFORE simple $@ to avoid conflicts\n\tresult = result.replace(/\\$\\{@:(\\d+)(?::(\\d+))?\\}/g, (_, startStr, lengthStr) => {\n\t\tlet start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)\n\t\t// Treat 0 as 1 (bash convention: args start at 1)\n\t\tif (start < 0) start = 0;\n\n\t\tif (lengthStr) {\n\t\t\tconst length = parseInt(lengthStr, 10);\n\t\t\treturn args.slice(start, start + length).join(\" \");\n\t\t}\n\t\treturn args.slice(start).join(\" \");\n\t});\n\n\t// Pre-compute all args joined (optimization)\n\tconst allArgs = args.join(\" \");\n\n\t// Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)\n\tresult = result.replace(/\\$ARGUMENTS/g, allArgs);\n\n\t// Replace $@ with all args joined (existing syntax)\n\tresult = result.replace(/\\$@/g, allArgs);\n\n\treturn result;\n}\n\nfunction loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null {\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);\n\n\t\tconst name = basename(filePath).replace(/\\.md$/, \"\");\n\n\t\t// Get description from frontmatter or first non-empty line\n\t\tlet description = frontmatter.description || \"\";\n\t\tif (!description) {\n\t\t\tconst firstLine = body.split(\"\\n\").find((line) => line.trim());\n\t\t\tif (firstLine) {\n\t\t\t\t// Truncate if too long\n\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t}\n\t\t}\n\n\t\t// Append source to description\n\t\tdescription = description ? `${description} ${sourceLabel}` : sourceLabel;\n\n\t\treturn {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tcontent: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Scan a directory for .md files (non-recursive) and load them as prompt templates.\n */\nfunction loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] {\n\tconst templates: PromptTemplate[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn templates;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a file\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(fullPath, source, sourceLabel);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn templates;\n\t}\n\n\treturn templates;\n}\n\nexport interface LoadPromptTemplatesOptions {\n\t/** Working directory for project-local templates. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global templates. Default: from getPromptsDir() */\n\tagentDir?: string;\n\t/** Explicit prompt template paths (files or directories) */\n\tpromptPaths?: string[];\n\t/** Include default prompt directories. Default: true */\n\tincludeDefaults?: boolean;\n}\n\nfunction normalizePath(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed === \"~\") return homedir();\n\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\treturn trimmed;\n}\n\nfunction resolvePromptPath(p: string, cwd: string): string {\n\tconst normalized = normalizePath(p);\n\treturn isAbsolute(normalized) ? normalized : resolve(cwd, normalized);\n}\n\nfunction buildPathSourceLabel(p: string): string {\n\tconst base = basename(p).replace(/\\.md$/, \"\") || \"path\";\n\treturn `(path:${base})`;\n}\n\n/**\n * Load all prompt templates from:\n * 1. Global: agentDir/prompts/\n * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/\n * 3. Explicit prompt paths\n */\nexport function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {\n\tconst resolvedCwd = options.cwd ?? process.cwd();\n\tconst resolvedAgentDir = options.agentDir ?? getPromptsDir();\n\tconst promptPaths = options.promptPaths ?? [];\n\tconst includeDefaults = options.includeDefaults ?? true;\n\n\tconst templates: PromptTemplate[] = [];\n\n\t// 0. Always load shipped (built-in) templates — part of the package, not user config\n\tconst shippedPromptsDir = getShippedPromptsDir();\n\tif (existsSync(shippedPromptsDir)) {\n\t\ttemplates.push(...loadTemplatesFromDir(shippedPromptsDir, \"builtin\", \"(builtin)\"));\n\t\t// Also scan subdirectories (e.g., commands/, agents/)\n\t\ttry {\n\t\t\tconst subdirs = readdirSync(shippedPromptsDir, { withFileTypes: true });\n\t\t\tfor (const entry of subdirs) {\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\ttemplates.push(...loadTemplatesFromDir(join(shippedPromptsDir, entry.name), \"builtin\", \"(builtin)\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tif (includeDefaults) {\n\t\t// 1. Load global templates from agentDir/prompts/\n\t\t// Note: if agentDir is provided, it should be the agent dir, not the prompts dir\n\t\tconst globalPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\t\ttemplates.push(...loadTemplatesFromDir(globalPromptsDir, \"user\", \"(user)\"));\n\n\t\t// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/\n\t\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\t\ttemplates.push(...loadTemplatesFromDir(projectPromptsDir, \"project\", \"(project)\"));\n\t}\n\n\tconst userPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\n\tconst isUnderPath = (target: string, root: string): boolean => {\n\t\tconst normalizedRoot = resolve(root);\n\t\tif (target === normalizedRoot) {\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n\t\treturn target.startsWith(prefix);\n\t};\n\n\tconst getSourceInfo = (resolvedPath: string): { source: string; label: string } => {\n\t\tif (!includeDefaults) {\n\t\t\tif (isUnderPath(resolvedPath, userPromptsDir)) {\n\t\t\t\treturn { source: \"user\", label: \"(user)\" };\n\t\t\t}\n\t\t\tif (isUnderPath(resolvedPath, projectPromptsDir)) {\n\t\t\t\treturn { source: \"project\", label: \"(project)\" };\n\t\t\t}\n\t\t}\n\t\treturn { source: \"path\", label: buildPathSourceLabel(resolvedPath) };\n\t};\n\n\t// 3. Load explicit prompt paths\n\tfor (const rawPath of promptPaths) {\n\t\tconst resolvedPath = resolvePromptPath(rawPath, resolvedCwd);\n\t\tif (!existsSync(resolvedPath)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolvedPath);\n\t\t\tconst { source, label } = getSourceInfo(resolvedPath);\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\ttemplates.push(...loadTemplatesFromDir(resolvedPath, source, label));\n\t\t\t} else if (stats.isFile() && resolvedPath.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(resolvedPath, source, label);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore read failures\n\t\t}\n\t}\n\n\treturn templates;\n}\n\n/**\n * Expand a prompt template if it matches a template name.\n * Returns the expanded content or the original text if not a template.\n */\nexport function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst template = templates.find((t) => t.name === templateName);\n\tif (template) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(template.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"prompt-templates.d.ts","sourceRoot":"","sources":["../../src/core/prompt-templates.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAkCtE;AA+ED,MAAM,WAAW,0BAA0B;IAC1C,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAoBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,0BAA+B,GAAG,cAAc,EAAE,CAoF9F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,MAAM,CActF","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, isAbsolute, join, resolve, sep } from \"path\";\nimport { CONFIG_DIR_NAME, getPromptsDir, getShippedPromptsDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\n/**\n * Represents a prompt template loaded from a markdown file\n */\nexport interface PromptTemplate {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // \"user\", \"project\", or \"path\"\n\tfilePath: string; // Absolute path to the template file\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in template content\n * Supports:\n * - $1, $2, ... for positional args\n * - $@ and $ARGUMENTS for all args\n * - ${@:N} for args from Nth onwards (bash-style slicing)\n * - ${@:N:L} for L args starting from Nth\n *\n * Note: Replacement happens on the template string only. Argument values\n * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $1, $2, etc. with positional args FIRST (before wildcards)\n\t// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\t// Replace ${@:start} or ${@:start:length} with sliced args (bash-style)\n\t// Process BEFORE simple $@ to avoid conflicts\n\tresult = result.replace(/\\$\\{@:(\\d+)(?::(\\d+))?\\}/g, (_, startStr, lengthStr) => {\n\t\tlet start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)\n\t\t// Treat 0 as 1 (bash convention: args start at 1)\n\t\tif (start < 0) start = 0;\n\n\t\tif (lengthStr) {\n\t\t\tconst length = parseInt(lengthStr, 10);\n\t\t\treturn args.slice(start, start + length).join(\" \");\n\t\t}\n\t\treturn args.slice(start).join(\" \");\n\t});\n\n\t// Pre-compute all args joined (optimization)\n\tconst allArgs = args.join(\" \");\n\n\t// Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)\n\tresult = result.replace(/\\$ARGUMENTS/g, allArgs);\n\n\t// Replace $@ with all args joined (existing syntax)\n\tresult = result.replace(/\\$@/g, allArgs);\n\n\treturn result;\n}\n\nfunction loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null {\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);\n\n\t\tconst name = basename(filePath).replace(/\\.md$/, \"\");\n\n\t\t// Get description from frontmatter or first non-empty line\n\t\tlet description = frontmatter.description || \"\";\n\t\tif (!description) {\n\t\t\tconst firstLine = body.split(\"\\n\").find((line) => line.trim());\n\t\t\tif (firstLine) {\n\t\t\t\t// Truncate if too long\n\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t}\n\t\t}\n\n\t\t// Append source to description (skip for builtins — not useful for the user)\n\t\tif (sourceLabel && sourceLabel !== \"(builtin)\") {\n\t\t\tdescription = description ? `${description} ${sourceLabel}` : sourceLabel;\n\t\t}\n\n\t\treturn {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tcontent: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Scan a directory for .md files (non-recursive) and load them as prompt templates.\n */\nfunction loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] {\n\tconst templates: PromptTemplate[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn templates;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a file\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(fullPath, source, sourceLabel);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn templates;\n\t}\n\n\treturn templates;\n}\n\nexport interface LoadPromptTemplatesOptions {\n\t/** Working directory for project-local templates. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global templates. Default: from getPromptsDir() */\n\tagentDir?: string;\n\t/** Explicit prompt template paths (files or directories) */\n\tpromptPaths?: string[];\n\t/** Include default prompt directories. Default: true */\n\tincludeDefaults?: boolean;\n}\n\nfunction normalizePath(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed === \"~\") return homedir();\n\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\treturn trimmed;\n}\n\nfunction resolvePromptPath(p: string, cwd: string): string {\n\tconst normalized = normalizePath(p);\n\treturn isAbsolute(normalized) ? normalized : resolve(cwd, normalized);\n}\n\nfunction buildPathSourceLabel(p: string): string {\n\tconst base = basename(p).replace(/\\.md$/, \"\") || \"path\";\n\treturn `(path:${base})`;\n}\n\n/**\n * Load all prompt templates from:\n * 1. Global: agentDir/prompts/\n * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/\n * 3. Explicit prompt paths\n */\nexport function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {\n\tconst resolvedCwd = options.cwd ?? process.cwd();\n\tconst resolvedAgentDir = options.agentDir ?? getPromptsDir();\n\tconst promptPaths = options.promptPaths ?? [];\n\tconst includeDefaults = options.includeDefaults ?? true;\n\n\tconst templates: PromptTemplate[] = [];\n\n\t// 0. Always load shipped (built-in) templates — part of the package, not user config\n\tconst shippedPromptsDir = getShippedPromptsDir();\n\tif (existsSync(shippedPromptsDir)) {\n\t\ttemplates.push(...loadTemplatesFromDir(shippedPromptsDir, \"builtin\", \"(builtin)\"));\n\t\t// Also scan subdirectories (e.g., commands/, agents/)\n\t\ttry {\n\t\t\tconst subdirs = readdirSync(shippedPromptsDir, { withFileTypes: true });\n\t\t\tfor (const entry of subdirs) {\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\ttemplates.push(...loadTemplatesFromDir(join(shippedPromptsDir, entry.name), \"builtin\", \"(builtin)\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tif (includeDefaults) {\n\t\t// 1. Load global templates from agentDir/prompts/\n\t\t// Note: if agentDir is provided, it should be the agent dir, not the prompts dir\n\t\tconst globalPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\t\ttemplates.push(...loadTemplatesFromDir(globalPromptsDir, \"user\", \"(user)\"));\n\n\t\t// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/\n\t\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\t\ttemplates.push(...loadTemplatesFromDir(projectPromptsDir, \"project\", \"(project)\"));\n\t}\n\n\tconst userPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\n\tconst isUnderPath = (target: string, root: string): boolean => {\n\t\tconst normalizedRoot = resolve(root);\n\t\tif (target === normalizedRoot) {\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n\t\treturn target.startsWith(prefix);\n\t};\n\n\tconst getSourceInfo = (resolvedPath: string): { source: string; label: string } => {\n\t\tif (!includeDefaults) {\n\t\t\tif (isUnderPath(resolvedPath, userPromptsDir)) {\n\t\t\t\treturn { source: \"user\", label: \"(user)\" };\n\t\t\t}\n\t\t\tif (isUnderPath(resolvedPath, projectPromptsDir)) {\n\t\t\t\treturn { source: \"project\", label: \"(project)\" };\n\t\t\t}\n\t\t}\n\t\treturn { source: \"path\", label: buildPathSourceLabel(resolvedPath) };\n\t};\n\n\t// 3. Load explicit prompt paths\n\tfor (const rawPath of promptPaths) {\n\t\tconst resolvedPath = resolvePromptPath(rawPath, resolvedCwd);\n\t\tif (!existsSync(resolvedPath)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolvedPath);\n\t\t\tconst { source, label } = getSourceInfo(resolvedPath);\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\ttemplates.push(...loadTemplatesFromDir(resolvedPath, source, label));\n\t\t\t} else if (stats.isFile() && resolvedPath.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(resolvedPath, source, label);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore read failures\n\t\t}\n\t}\n\n\treturn templates;\n}\n\n/**\n * Expand a prompt template if it matches a template name.\n * Returns the expanded content or the original text if not a template.\n */\nexport function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst template = templates.find((t) => t.name === templateName);\n\tif (template) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(template.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
@@ -95,8 +95,10 @@ function loadTemplateFromFile(filePath, source, sourceLabel) {
|
|
|
95
95
|
description += "...";
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
// Append source to description
|
|
99
|
-
|
|
98
|
+
// Append source to description (skip for builtins — not useful for the user)
|
|
99
|
+
if (sourceLabel && sourceLabel !== "(builtin)") {
|
|
100
|
+
description = description ? `${description} ${sourceLabel}` : sourceLabel;
|
|
101
|
+
}
|
|
100
102
|
return {
|
|
101
103
|
name,
|
|
102
104
|
description,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-templates.js","sourceRoot":"","sources":["../../src/core/prompt-templates.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, isAbsolute, join, resolve, sep } from \"path\";\nimport { CONFIG_DIR_NAME, getPromptsDir, getShippedPromptsDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\n/**\n * Represents a prompt template loaded from a markdown file\n */\nexport interface PromptTemplate {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // \"user\", \"project\", or \"path\"\n\tfilePath: string; // Absolute path to the template file\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in template content\n * Supports:\n * - $1, $2, ... for positional args\n * - $@ and $ARGUMENTS for all args\n * - ${@:N} for args from Nth onwards (bash-style slicing)\n * - ${@:N:L} for L args starting from Nth\n *\n * Note: Replacement happens on the template string only. Argument values\n * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $1, $2, etc. with positional args FIRST (before wildcards)\n\t// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\t// Replace ${@:start} or ${@:start:length} with sliced args (bash-style)\n\t// Process BEFORE simple $@ to avoid conflicts\n\tresult = result.replace(/\\$\\{@:(\\d+)(?::(\\d+))?\\}/g, (_, startStr, lengthStr) => {\n\t\tlet start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)\n\t\t// Treat 0 as 1 (bash convention: args start at 1)\n\t\tif (start < 0) start = 0;\n\n\t\tif (lengthStr) {\n\t\t\tconst length = parseInt(lengthStr, 10);\n\t\t\treturn args.slice(start, start + length).join(\" \");\n\t\t}\n\t\treturn args.slice(start).join(\" \");\n\t});\n\n\t// Pre-compute all args joined (optimization)\n\tconst allArgs = args.join(\" \");\n\n\t// Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)\n\tresult = result.replace(/\\$ARGUMENTS/g, allArgs);\n\n\t// Replace $@ with all args joined (existing syntax)\n\tresult = result.replace(/\\$@/g, allArgs);\n\n\treturn result;\n}\n\nfunction loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null {\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);\n\n\t\tconst name = basename(filePath).replace(/\\.md$/, \"\");\n\n\t\t// Get description from frontmatter or first non-empty line\n\t\tlet description = frontmatter.description || \"\";\n\t\tif (!description) {\n\t\t\tconst firstLine = body.split(\"\\n\").find((line) => line.trim());\n\t\t\tif (firstLine) {\n\t\t\t\t// Truncate if too long\n\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t}\n\t\t}\n\n\t\t// Append source to description\n\t\tdescription = description ? `${description} ${sourceLabel}` : sourceLabel;\n\n\t\treturn {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tcontent: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Scan a directory for .md files (non-recursive) and load them as prompt templates.\n */\nfunction loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] {\n\tconst templates: PromptTemplate[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn templates;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a file\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(fullPath, source, sourceLabel);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn templates;\n\t}\n\n\treturn templates;\n}\n\nexport interface LoadPromptTemplatesOptions {\n\t/** Working directory for project-local templates. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global templates. Default: from getPromptsDir() */\n\tagentDir?: string;\n\t/** Explicit prompt template paths (files or directories) */\n\tpromptPaths?: string[];\n\t/** Include default prompt directories. Default: true */\n\tincludeDefaults?: boolean;\n}\n\nfunction normalizePath(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed === \"~\") return homedir();\n\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\treturn trimmed;\n}\n\nfunction resolvePromptPath(p: string, cwd: string): string {\n\tconst normalized = normalizePath(p);\n\treturn isAbsolute(normalized) ? normalized : resolve(cwd, normalized);\n}\n\nfunction buildPathSourceLabel(p: string): string {\n\tconst base = basename(p).replace(/\\.md$/, \"\") || \"path\";\n\treturn `(path:${base})`;\n}\n\n/**\n * Load all prompt templates from:\n * 1. Global: agentDir/prompts/\n * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/\n * 3. Explicit prompt paths\n */\nexport function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {\n\tconst resolvedCwd = options.cwd ?? process.cwd();\n\tconst resolvedAgentDir = options.agentDir ?? getPromptsDir();\n\tconst promptPaths = options.promptPaths ?? [];\n\tconst includeDefaults = options.includeDefaults ?? true;\n\n\tconst templates: PromptTemplate[] = [];\n\n\t// 0. Always load shipped (built-in) templates — part of the package, not user config\n\tconst shippedPromptsDir = getShippedPromptsDir();\n\tif (existsSync(shippedPromptsDir)) {\n\t\ttemplates.push(...loadTemplatesFromDir(shippedPromptsDir, \"builtin\", \"(builtin)\"));\n\t\t// Also scan subdirectories (e.g., commands/, agents/)\n\t\ttry {\n\t\t\tconst subdirs = readdirSync(shippedPromptsDir, { withFileTypes: true });\n\t\t\tfor (const entry of subdirs) {\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\ttemplates.push(...loadTemplatesFromDir(join(shippedPromptsDir, entry.name), \"builtin\", \"(builtin)\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tif (includeDefaults) {\n\t\t// 1. Load global templates from agentDir/prompts/\n\t\t// Note: if agentDir is provided, it should be the agent dir, not the prompts dir\n\t\tconst globalPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\t\ttemplates.push(...loadTemplatesFromDir(globalPromptsDir, \"user\", \"(user)\"));\n\n\t\t// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/\n\t\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\t\ttemplates.push(...loadTemplatesFromDir(projectPromptsDir, \"project\", \"(project)\"));\n\t}\n\n\tconst userPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\n\tconst isUnderPath = (target: string, root: string): boolean => {\n\t\tconst normalizedRoot = resolve(root);\n\t\tif (target === normalizedRoot) {\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n\t\treturn target.startsWith(prefix);\n\t};\n\n\tconst getSourceInfo = (resolvedPath: string): { source: string; label: string } => {\n\t\tif (!includeDefaults) {\n\t\t\tif (isUnderPath(resolvedPath, userPromptsDir)) {\n\t\t\t\treturn { source: \"user\", label: \"(user)\" };\n\t\t\t}\n\t\t\tif (isUnderPath(resolvedPath, projectPromptsDir)) {\n\t\t\t\treturn { source: \"project\", label: \"(project)\" };\n\t\t\t}\n\t\t}\n\t\treturn { source: \"path\", label: buildPathSourceLabel(resolvedPath) };\n\t};\n\n\t// 3. Load explicit prompt paths\n\tfor (const rawPath of promptPaths) {\n\t\tconst resolvedPath = resolvePromptPath(rawPath, resolvedCwd);\n\t\tif (!existsSync(resolvedPath)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolvedPath);\n\t\t\tconst { source, label } = getSourceInfo(resolvedPath);\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\ttemplates.push(...loadTemplatesFromDir(resolvedPath, source, label));\n\t\t\t} else if (stats.isFile() && resolvedPath.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(resolvedPath, source, label);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore read failures\n\t\t}\n\t}\n\n\treturn templates;\n}\n\n/**\n * Expand a prompt template if it matches a template name.\n * Returns the expanded content or the original text if not a template.\n */\nexport function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst template = templates.find((t) => t.name === templateName);\n\tif (template) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(template.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"prompt-templates.js","sourceRoot":"","sources":["../../src/core/prompt-templates.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, isAbsolute, join, resolve, sep } from \"path\";\nimport { CONFIG_DIR_NAME, getPromptsDir, getShippedPromptsDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\n/**\n * Represents a prompt template loaded from a markdown file\n */\nexport interface PromptTemplate {\n\tname: string;\n\tdescription: string;\n\tcontent: string;\n\tsource: string; // \"user\", \"project\", or \"path\"\n\tfilePath: string; // Absolute path to the template file\n}\n\n/**\n * Parse command arguments respecting quoted strings (bash-style)\n * Returns array of arguments\n */\nexport function parseCommandArgs(argsString: string): string[] {\n\tconst args: string[] = [];\n\tlet current = \"\";\n\tlet inQuote: string | null = null;\n\n\tfor (let i = 0; i < argsString.length; i++) {\n\t\tconst char = argsString[i];\n\n\t\tif (inQuote) {\n\t\t\tif (char === inQuote) {\n\t\t\t\tinQuote = null;\n\t\t\t} else {\n\t\t\t\tcurrent += char;\n\t\t\t}\n\t\t} else if (char === '\"' || char === \"'\") {\n\t\t\tinQuote = char;\n\t\t} else if (char === \" \" || char === \"\\t\") {\n\t\t\tif (current) {\n\t\t\t\targs.push(current);\n\t\t\t\tcurrent = \"\";\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent += char;\n\t\t}\n\t}\n\n\tif (current) {\n\t\targs.push(current);\n\t}\n\n\treturn args;\n}\n\n/**\n * Substitute argument placeholders in template content\n * Supports:\n * - $1, $2, ... for positional args\n * - $@ and $ARGUMENTS for all args\n * - ${@:N} for args from Nth onwards (bash-style slicing)\n * - ${@:N:L} for L args starting from Nth\n *\n * Note: Replacement happens on the template string only. Argument values\n * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.\n */\nexport function substituteArgs(content: string, args: string[]): string {\n\tlet result = content;\n\n\t// Replace $1, $2, etc. with positional args FIRST (before wildcards)\n\t// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted\n\tresult = result.replace(/\\$(\\d+)/g, (_, num) => {\n\t\tconst index = parseInt(num, 10) - 1;\n\t\treturn args[index] ?? \"\";\n\t});\n\n\t// Replace ${@:start} or ${@:start:length} with sliced args (bash-style)\n\t// Process BEFORE simple $@ to avoid conflicts\n\tresult = result.replace(/\\$\\{@:(\\d+)(?::(\\d+))?\\}/g, (_, startStr, lengthStr) => {\n\t\tlet start = parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed)\n\t\t// Treat 0 as 1 (bash convention: args start at 1)\n\t\tif (start < 0) start = 0;\n\n\t\tif (lengthStr) {\n\t\t\tconst length = parseInt(lengthStr, 10);\n\t\t\treturn args.slice(start, start + length).join(\" \");\n\t\t}\n\t\treturn args.slice(start).join(\" \");\n\t});\n\n\t// Pre-compute all args joined (optimization)\n\tconst allArgs = args.join(\" \");\n\n\t// Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)\n\tresult = result.replace(/\\$ARGUMENTS/g, allArgs);\n\n\t// Replace $@ with all args joined (existing syntax)\n\tresult = result.replace(/\\$@/g, allArgs);\n\n\treturn result;\n}\n\nfunction loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null {\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);\n\n\t\tconst name = basename(filePath).replace(/\\.md$/, \"\");\n\n\t\t// Get description from frontmatter or first non-empty line\n\t\tlet description = frontmatter.description || \"\";\n\t\tif (!description) {\n\t\t\tconst firstLine = body.split(\"\\n\").find((line) => line.trim());\n\t\t\tif (firstLine) {\n\t\t\t\t// Truncate if too long\n\t\t\t\tdescription = firstLine.slice(0, 60);\n\t\t\t\tif (firstLine.length > 60) description += \"...\";\n\t\t\t}\n\t\t}\n\n\t\t// Append source to description (skip for builtins — not useful for the user)\n\t\tif (sourceLabel && sourceLabel !== \"(builtin)\") {\n\t\t\tdescription = description ? `${description} ${sourceLabel}` : sourceLabel;\n\t\t}\n\n\t\treturn {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tcontent: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Scan a directory for .md files (non-recursive) and load them as prompt templates.\n */\nfunction loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] {\n\tconst templates: PromptTemplate[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn templates;\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a file\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isFile && entry.name.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(fullPath, source, sourceLabel);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn templates;\n\t}\n\n\treturn templates;\n}\n\nexport interface LoadPromptTemplatesOptions {\n\t/** Working directory for project-local templates. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global templates. Default: from getPromptsDir() */\n\tagentDir?: string;\n\t/** Explicit prompt template paths (files or directories) */\n\tpromptPaths?: string[];\n\t/** Include default prompt directories. Default: true */\n\tincludeDefaults?: boolean;\n}\n\nfunction normalizePath(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed === \"~\") return homedir();\n\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\treturn trimmed;\n}\n\nfunction resolvePromptPath(p: string, cwd: string): string {\n\tconst normalized = normalizePath(p);\n\treturn isAbsolute(normalized) ? normalized : resolve(cwd, normalized);\n}\n\nfunction buildPathSourceLabel(p: string): string {\n\tconst base = basename(p).replace(/\\.md$/, \"\") || \"path\";\n\treturn `(path:${base})`;\n}\n\n/**\n * Load all prompt templates from:\n * 1. Global: agentDir/prompts/\n * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/\n * 3. Explicit prompt paths\n */\nexport function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {\n\tconst resolvedCwd = options.cwd ?? process.cwd();\n\tconst resolvedAgentDir = options.agentDir ?? getPromptsDir();\n\tconst promptPaths = options.promptPaths ?? [];\n\tconst includeDefaults = options.includeDefaults ?? true;\n\n\tconst templates: PromptTemplate[] = [];\n\n\t// 0. Always load shipped (built-in) templates — part of the package, not user config\n\tconst shippedPromptsDir = getShippedPromptsDir();\n\tif (existsSync(shippedPromptsDir)) {\n\t\ttemplates.push(...loadTemplatesFromDir(shippedPromptsDir, \"builtin\", \"(builtin)\"));\n\t\t// Also scan subdirectories (e.g., commands/, agents/)\n\t\ttry {\n\t\t\tconst subdirs = readdirSync(shippedPromptsDir, { withFileTypes: true });\n\t\t\tfor (const entry of subdirs) {\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\ttemplates.push(...loadTemplatesFromDir(join(shippedPromptsDir, entry.name), \"builtin\", \"(builtin)\"));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\tif (includeDefaults) {\n\t\t// 1. Load global templates from agentDir/prompts/\n\t\t// Note: if agentDir is provided, it should be the agent dir, not the prompts dir\n\t\tconst globalPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\t\ttemplates.push(...loadTemplatesFromDir(globalPromptsDir, \"user\", \"(user)\"));\n\n\t\t// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/\n\t\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\t\ttemplates.push(...loadTemplatesFromDir(projectPromptsDir, \"project\", \"(project)\"));\n\t}\n\n\tconst userPromptsDir = options.agentDir ? join(options.agentDir, \"prompts\") : resolvedAgentDir;\n\tconst projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, \"prompts\");\n\n\tconst isUnderPath = (target: string, root: string): boolean => {\n\t\tconst normalizedRoot = resolve(root);\n\t\tif (target === normalizedRoot) {\n\t\t\treturn true;\n\t\t}\n\t\tconst prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n\t\treturn target.startsWith(prefix);\n\t};\n\n\tconst getSourceInfo = (resolvedPath: string): { source: string; label: string } => {\n\t\tif (!includeDefaults) {\n\t\t\tif (isUnderPath(resolvedPath, userPromptsDir)) {\n\t\t\t\treturn { source: \"user\", label: \"(user)\" };\n\t\t\t}\n\t\t\tif (isUnderPath(resolvedPath, projectPromptsDir)) {\n\t\t\t\treturn { source: \"project\", label: \"(project)\" };\n\t\t\t}\n\t\t}\n\t\treturn { source: \"path\", label: buildPathSourceLabel(resolvedPath) };\n\t};\n\n\t// 3. Load explicit prompt paths\n\tfor (const rawPath of promptPaths) {\n\t\tconst resolvedPath = resolvePromptPath(rawPath, resolvedCwd);\n\t\tif (!existsSync(resolvedPath)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolvedPath);\n\t\t\tconst { source, label } = getSourceInfo(resolvedPath);\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\ttemplates.push(...loadTemplatesFromDir(resolvedPath, source, label));\n\t\t\t} else if (stats.isFile() && resolvedPath.endsWith(\".md\")) {\n\t\t\t\tconst template = loadTemplateFromFile(resolvedPath, source, label);\n\t\t\t\tif (template) {\n\t\t\t\t\ttemplates.push(template);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore read failures\n\t\t}\n\t}\n\n\treturn templates;\n}\n\n/**\n * Expand a prompt template if it matches a template name.\n * Returns the expanded content or the original text if not a template.\n */\nexport function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst templateName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst argsString = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\tconst template = templates.find((t) => t.name === templateName);\n\tif (template) {\n\t\tconst args = parseCommandArgs(argsString);\n\t\treturn substituteArgs(template.content, args);\n\t}\n\n\treturn text;\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract PascalCase glossary terms from an "## Ubiquitous Language" section.
|
|
3
|
+
* Handles three formats:
|
|
4
|
+
* - **Term** bold format
|
|
5
|
+
* - - Term: list format
|
|
6
|
+
* - | Term | table format
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractGlossaryTerms(content: string): Set<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Returns terms from candidateTerms that are not present in the glossary.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateDomainGlossary(glossaryContent: string, candidateTerms: string[]): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Load domain content from .planning/DOMAIN-MODEL.md (preferred) or DOMAIN.md.
|
|
15
|
+
* Returns empty string if neither exists.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadDomainContent(cwd: string): string;
|
|
18
|
+
//# sourceMappingURL=domain-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-validator.d.ts","sourceRoot":"","sources":["../../src/gsd/domain-validator.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAuBjE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIlG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWrD","sourcesContent":["// GSD Domain Validator — glossary extraction and domain naming validation.\n// Reads DOMAIN-MODEL.md (preferred) or DOMAIN.md as the glossary source of truth.\n// Used by draht-quality-gate.js and exported via @draht/coding-agent.\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n/**\n * Extract PascalCase glossary terms from an \"## Ubiquitous Language\" section.\n * Handles three formats:\n * - **Term** bold format\n * - - Term: list format\n * - | Term | table format\n */\nexport function extractGlossaryTerms(content: string): Set<string> {\n\tconst terms = new Set<string>();\n\tif (!content) return terms;\n\n\t// Extract only the Ubiquitous Language section (stop at next ## heading)\n\tconst sectionMatch = content.match(/## Ubiquitous Language([\\s\\S]*?)(?:\\n## |\\s*$)/);\n\tconst section = sectionMatch ? sectionMatch[1] : \"\";\n\tif (!section) return terms;\n\n\t// **PascalCase** bold format\n\tfor (const m of section.matchAll(/\\*\\*([A-Z][a-zA-Z0-9]+)\\*\\*/g)) {\n\t\tterms.add(m[1]);\n\t}\n\t// - PascalCase: list format\n\tfor (const m of section.matchAll(/^[-*]\\s+([A-Z][a-zA-Z0-9]+)\\s*:/gm)) {\n\t\tterms.add(m[1]);\n\t}\n\t// | PascalCase | table format\n\tfor (const m of section.matchAll(/\\|\\s*([A-Z][a-zA-Z0-9]+)\\s*\\|/g)) {\n\t\tterms.add(m[1]);\n\t}\n\n\treturn terms;\n}\n\n/**\n * Returns terms from candidateTerms that are not present in the glossary.\n */\nexport function validateDomainGlossary(glossaryContent: string, candidateTerms: string[]): string[] {\n\tif (candidateTerms.length === 0) return [];\n\tconst known = extractGlossaryTerms(glossaryContent);\n\treturn candidateTerms.filter((t) => !known.has(t));\n}\n\n/**\n * Load domain content from .planning/DOMAIN-MODEL.md (preferred) or DOMAIN.md.\n * Returns empty string if neither exists.\n */\nexport function loadDomainContent(cwd: string): string {\n\tconst planningDir = path.join(cwd, \".planning\");\n\tconst modelPath = path.join(planningDir, \"DOMAIN-MODEL.md\");\n\tif (fs.existsSync(modelPath)) {\n\t\treturn fs.readFileSync(modelPath, \"utf-8\");\n\t}\n\tconst domainPath = path.join(planningDir, \"DOMAIN.md\");\n\tif (fs.existsSync(domainPath)) {\n\t\treturn fs.readFileSync(domainPath, \"utf-8\");\n\t}\n\treturn \"\";\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// GSD Domain Validator — glossary extraction and domain naming validation.
|
|
2
|
+
// Reads DOMAIN-MODEL.md (preferred) or DOMAIN.md as the glossary source of truth.
|
|
3
|
+
// Used by draht-quality-gate.js and exported via @draht/coding-agent.
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
/**
|
|
7
|
+
* Extract PascalCase glossary terms from an "## Ubiquitous Language" section.
|
|
8
|
+
* Handles three formats:
|
|
9
|
+
* - **Term** bold format
|
|
10
|
+
* - - Term: list format
|
|
11
|
+
* - | Term | table format
|
|
12
|
+
*/
|
|
13
|
+
export function extractGlossaryTerms(content) {
|
|
14
|
+
const terms = new Set();
|
|
15
|
+
if (!content)
|
|
16
|
+
return terms;
|
|
17
|
+
// Extract only the Ubiquitous Language section (stop at next ## heading)
|
|
18
|
+
const sectionMatch = content.match(/## Ubiquitous Language([\s\S]*?)(?:\n## |\s*$)/);
|
|
19
|
+
const section = sectionMatch ? sectionMatch[1] : "";
|
|
20
|
+
if (!section)
|
|
21
|
+
return terms;
|
|
22
|
+
// **PascalCase** bold format
|
|
23
|
+
for (const m of section.matchAll(/\*\*([A-Z][a-zA-Z0-9]+)\*\*/g)) {
|
|
24
|
+
terms.add(m[1]);
|
|
25
|
+
}
|
|
26
|
+
// - PascalCase: list format
|
|
27
|
+
for (const m of section.matchAll(/^[-*]\s+([A-Z][a-zA-Z0-9]+)\s*:/gm)) {
|
|
28
|
+
terms.add(m[1]);
|
|
29
|
+
}
|
|
30
|
+
// | PascalCase | table format
|
|
31
|
+
for (const m of section.matchAll(/\|\s*([A-Z][a-zA-Z0-9]+)\s*\|/g)) {
|
|
32
|
+
terms.add(m[1]);
|
|
33
|
+
}
|
|
34
|
+
return terms;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns terms from candidateTerms that are not present in the glossary.
|
|
38
|
+
*/
|
|
39
|
+
export function validateDomainGlossary(glossaryContent, candidateTerms) {
|
|
40
|
+
if (candidateTerms.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const known = extractGlossaryTerms(glossaryContent);
|
|
43
|
+
return candidateTerms.filter((t) => !known.has(t));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load domain content from .planning/DOMAIN-MODEL.md (preferred) or DOMAIN.md.
|
|
47
|
+
* Returns empty string if neither exists.
|
|
48
|
+
*/
|
|
49
|
+
export function loadDomainContent(cwd) {
|
|
50
|
+
const planningDir = path.join(cwd, ".planning");
|
|
51
|
+
const modelPath = path.join(planningDir, "DOMAIN-MODEL.md");
|
|
52
|
+
if (fs.existsSync(modelPath)) {
|
|
53
|
+
return fs.readFileSync(modelPath, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
const domainPath = path.join(planningDir, "DOMAIN.md");
|
|
56
|
+
if (fs.existsSync(domainPath)) {
|
|
57
|
+
return fs.readFileSync(domainPath, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=domain-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-validator.js","sourceRoot":"","sources":["../../src/gsd/domain-validator.ts"],"names":[],"mappings":"AAAA,6EAA2E;AAC3E,kFAAkF;AAClF,sEAAsE;AAEtE,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAe;IAClE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAE3B,yEAAyE;IACzE,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACrF,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAE3B,6BAA6B;IAC7B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,8BAA8B,CAAC,EAAE,CAAC;QAClE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,4BAA4B;IAC5B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,mCAAmC,CAAC,EAAE,CAAC;QACvE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,8BAA8B;IAC9B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,gCAAgC,CAAC,EAAE,CAAC;QACpE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,OAAO,KAAK,CAAC;AAAA,CACb;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,eAAuB,EAAE,cAAwB,EAAY;IACnG,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,oBAAoB,CAAC,eAAe,CAAC,CAAC;IACpD,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAAA,CACnD;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAU;IACtD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAC5D,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACvD,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,EAAE,CAAC;AAAA,CACV","sourcesContent":["// GSD Domain Validator — glossary extraction and domain naming validation.\n// Reads DOMAIN-MODEL.md (preferred) or DOMAIN.md as the glossary source of truth.\n// Used by draht-quality-gate.js and exported via @draht/coding-agent.\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n/**\n * Extract PascalCase glossary terms from an \"## Ubiquitous Language\" section.\n * Handles three formats:\n * - **Term** bold format\n * - - Term: list format\n * - | Term | table format\n */\nexport function extractGlossaryTerms(content: string): Set<string> {\n\tconst terms = new Set<string>();\n\tif (!content) return terms;\n\n\t// Extract only the Ubiquitous Language section (stop at next ## heading)\n\tconst sectionMatch = content.match(/## Ubiquitous Language([\\s\\S]*?)(?:\\n## |\\s*$)/);\n\tconst section = sectionMatch ? sectionMatch[1] : \"\";\n\tif (!section) return terms;\n\n\t// **PascalCase** bold format\n\tfor (const m of section.matchAll(/\\*\\*([A-Z][a-zA-Z0-9]+)\\*\\*/g)) {\n\t\tterms.add(m[1]);\n\t}\n\t// - PascalCase: list format\n\tfor (const m of section.matchAll(/^[-*]\\s+([A-Z][a-zA-Z0-9]+)\\s*:/gm)) {\n\t\tterms.add(m[1]);\n\t}\n\t// | PascalCase | table format\n\tfor (const m of section.matchAll(/\\|\\s*([A-Z][a-zA-Z0-9]+)\\s*\\|/g)) {\n\t\tterms.add(m[1]);\n\t}\n\n\treturn terms;\n}\n\n/**\n * Returns terms from candidateTerms that are not present in the glossary.\n */\nexport function validateDomainGlossary(glossaryContent: string, candidateTerms: string[]): string[] {\n\tif (candidateTerms.length === 0) return [];\n\tconst known = extractGlossaryTerms(glossaryContent);\n\treturn candidateTerms.filter((t) => !known.has(t));\n}\n\n/**\n * Load domain content from .planning/DOMAIN-MODEL.md (preferred) or DOMAIN.md.\n * Returns empty string if neither exists.\n */\nexport function loadDomainContent(cwd: string): string {\n\tconst planningDir = path.join(cwd, \".planning\");\n\tconst modelPath = path.join(planningDir, \"DOMAIN-MODEL.md\");\n\tif (fs.existsSync(modelPath)) {\n\t\treturn fs.readFileSync(modelPath, \"utf-8\");\n\t}\n\tconst domainPath = path.join(planningDir, \"DOMAIN.md\");\n\tif (fs.existsSync(domainPath)) {\n\t\treturn fs.readFileSync(domainPath, \"utf-8\");\n\t}\n\treturn \"\";\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a DOMAIN-MODEL.md scaffold from PROJECT.md.
|
|
3
|
+
* Requires .planning/PROJECT.md to exist.
|
|
4
|
+
* Returns the path to the created file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDomainModel(cwd: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Scan the codebase and write .planning/codebase/ analysis files.
|
|
9
|
+
* Returns array of created file paths.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mapCodebase(cwd: string): string[];
|
|
12
|
+
//# sourceMappingURL=domain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../src/gsd/domain.ts"],"names":[],"mappings":"AAsBA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqCrD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAoFjD","sourcesContent":["// GSD Domain module — domain model and codebase mapping operations.\n// Part of the draht GSD (Get Shit Done) methodology.\n// Exported via src/gsd/index.ts and @draht/coding-agent.\n\nimport { execSync } from \"node:child_process\";\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 timestamp(): string {\n\treturn new Date().toISOString().replace(\"T\", \" \").slice(0, 19);\n}\n\n/**\n * Generate a DOMAIN-MODEL.md scaffold from PROJECT.md.\n * Requires .planning/PROJECT.md to exist.\n * Returns the path to the created file.\n */\nexport function createDomainModel(cwd: string): string {\n\tconst projectPath = planningPath(cwd, \"PROJECT.md\");\n\tif (!fs.existsSync(projectPath)) {\n\t\tthrow new Error(\"No PROJECT.md found — run create-project first\");\n\t}\n\n\tconst outPath = planningPath(cwd, \"DOMAIN-MODEL.md\");\n\tconst 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`;\n\tfs.writeFileSync(outPath, tmpl, \"utf-8\");\n\treturn outPath;\n}\n\n/**\n * Scan the codebase and write .planning/codebase/ analysis files.\n * Returns array of created file paths.\n */\nexport function mapCodebase(cwd: string): string[] {\n\tconst outDir = planningPath(cwd, \"codebase\");\n\tensureDir(outDir);\n\n\t// Gather file tree\n\tlet tree = \"\";\n\ttry {\n\t\ttree = execSync(\n\t\t\t`find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.planning/*' | head -200`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t);\n\t} catch {\n\t\ttree = \"(unable to list files)\";\n\t}\n\n\t// Gather package info\n\tlet pkgJson: {\n\t\tname?: string;\n\t\tdependencies?: Record<string, string>;\n\t\tdevDependencies?: Record<string, string>;\n\t} | null = null;\n\ttry {\n\t\tpkgJson = JSON.parse(fs.readFileSync(path.join(cwd, \"package.json\"), \"utf-8\"));\n\t} catch {\n\t\t// not a Node.js project\n\t}\n\n\tconst stackPath = path.join(outDir, \"STACK.md\");\n\tfs.writeFileSync(\n\t\tstackPath,\n\t\t`# 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`,\n\t\t\"utf-8\",\n\t);\n\n\tconst archPath = path.join(outDir, \"ARCHITECTURE.md\");\n\tfs.writeFileSync(\n\t\tarchPath,\n\t\t`# Architecture\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Document file/directory patterns\\n- [ ] Map module boundaries\\n- [ ] Describe data flow\\n`,\n\t\t\"utf-8\",\n\t);\n\n\tconst convPath = path.join(outDir, \"CONVENTIONS.md\");\n\tfs.writeFileSync(\n\t\tconvPath,\n\t\t`# Conventions\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Document code style patterns\\n- [ ] Document testing patterns\\n- [ ] Document error handling approach\\n`,\n\t\t\"utf-8\",\n\t);\n\n\tconst concernsPath = path.join(outDir, \"CONCERNS.md\");\n\tfs.writeFileSync(\n\t\tconcernsPath,\n\t\t`# Concerns\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Identify technical debt\\n- [ ] Flag security concerns\\n- [ ] Note missing tests\\n`,\n\t\t\"utf-8\",\n\t);\n\n\t// Domain model extraction\n\tlet domainHints = \"\";\n\ttry {\n\t\tconst types = execSync(\n\t\t\t`grep -rn 'export\\\\s\\\\+\\\\(interface\\\\|type\\\\|class\\\\)' --include='*.ts' --include='*.go' . 2>/dev/null | grep -v node_modules | grep -v dist | head -50`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t).trim();\n\t\tif (types) domainHints += `## Types/Interfaces (potential entities)\\n\\`\\`\\`\\n${types}\\n\\`\\`\\`\\n\\n`;\n\t} catch {\n\t\t// no ts/go files\n\t}\n\ttry {\n\t\tconst dirs = execSync(\n\t\t\t`find . -type d -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | sort`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t).trim();\n\t\tif (dirs) domainHints += `## Directory Structure (potential bounded contexts)\\n\\`\\`\\`\\n${dirs}\\n\\`\\`\\`\\n`;\n\t} catch {\n\t\t// ignore\n\t}\n\n\tconst hintsPath = path.join(outDir, \"DOMAIN-HINTS.md\");\n\tfs.writeFileSync(\n\t\thintsPath,\n\t\t`# 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`,\n\t\t\"utf-8\",\n\t);\n\n\treturn [stackPath, archPath, convPath, concernsPath, hintsPath];\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// GSD Domain module — domain model and codebase mapping operations.
|
|
2
|
+
// Part of the draht GSD (Get Shit Done) methodology.
|
|
3
|
+
// Exported via src/gsd/index.ts and @draht/coding-agent.
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
const PLANNING = ".planning";
|
|
8
|
+
function planningPath(cwd, ...segments) {
|
|
9
|
+
return path.join(cwd, PLANNING, ...segments);
|
|
10
|
+
}
|
|
11
|
+
function ensureDir(dir) {
|
|
12
|
+
if (!fs.existsSync(dir))
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
function timestamp() {
|
|
16
|
+
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generate a DOMAIN-MODEL.md scaffold from PROJECT.md.
|
|
20
|
+
* Requires .planning/PROJECT.md to exist.
|
|
21
|
+
* Returns the path to the created file.
|
|
22
|
+
*/
|
|
23
|
+
export function createDomainModel(cwd) {
|
|
24
|
+
const projectPath = planningPath(cwd, "PROJECT.md");
|
|
25
|
+
if (!fs.existsSync(projectPath)) {
|
|
26
|
+
throw new Error("No PROJECT.md found — run create-project first");
|
|
27
|
+
}
|
|
28
|
+
const outPath = planningPath(cwd, "DOMAIN-MODEL.md");
|
|
29
|
+
const tmpl = `# Domain Model
|
|
30
|
+
|
|
31
|
+
## Bounded Contexts
|
|
32
|
+
[Extract from PROJECT.md — identify distinct areas of responsibility]
|
|
33
|
+
|
|
34
|
+
## Context Map
|
|
35
|
+
[How bounded contexts interact — upstream/downstream, shared kernel, etc.]
|
|
36
|
+
|
|
37
|
+
## Entities
|
|
38
|
+
[Core domain objects with identity]
|
|
39
|
+
|
|
40
|
+
## Value Objects
|
|
41
|
+
[Immutable objects defined by attributes]
|
|
42
|
+
|
|
43
|
+
## Aggregates
|
|
44
|
+
[Cluster of entities with a root — transactional boundary]
|
|
45
|
+
|
|
46
|
+
## Domain Events
|
|
47
|
+
[Things that happen in the domain]
|
|
48
|
+
|
|
49
|
+
## Ubiquitous Language Glossary
|
|
50
|
+
| Term | Context | Definition |
|
|
51
|
+
|------|---------|------------|
|
|
52
|
+
| [term] | [context] | [definition] |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
Generated from PROJECT.md: ${timestamp()}
|
|
56
|
+
`;
|
|
57
|
+
fs.writeFileSync(outPath, tmpl, "utf-8");
|
|
58
|
+
return outPath;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Scan the codebase and write .planning/codebase/ analysis files.
|
|
62
|
+
* Returns array of created file paths.
|
|
63
|
+
*/
|
|
64
|
+
export function mapCodebase(cwd) {
|
|
65
|
+
const outDir = planningPath(cwd, "codebase");
|
|
66
|
+
ensureDir(outDir);
|
|
67
|
+
// Gather file tree
|
|
68
|
+
let tree = "";
|
|
69
|
+
try {
|
|
70
|
+
tree = execSync(`find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.planning/*' | head -200`, { cwd, encoding: "utf-8" });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
tree = "(unable to list files)";
|
|
74
|
+
}
|
|
75
|
+
// Gather package info
|
|
76
|
+
let pkgJson = null;
|
|
77
|
+
try {
|
|
78
|
+
pkgJson = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// not a Node.js project
|
|
82
|
+
}
|
|
83
|
+
const stackPath = path.join(outDir, "STACK.md");
|
|
84
|
+
fs.writeFileSync(stackPath, `# 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`, "utf-8");
|
|
85
|
+
const archPath = path.join(outDir, "ARCHITECTURE.md");
|
|
86
|
+
fs.writeFileSync(archPath, `# Architecture\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Document file/directory patterns\n- [ ] Map module boundaries\n- [ ] Describe data flow\n`, "utf-8");
|
|
87
|
+
const convPath = path.join(outDir, "CONVENTIONS.md");
|
|
88
|
+
fs.writeFileSync(convPath, `# Conventions\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Document code style patterns\n- [ ] Document testing patterns\n- [ ] Document error handling approach\n`, "utf-8");
|
|
89
|
+
const concernsPath = path.join(outDir, "CONCERNS.md");
|
|
90
|
+
fs.writeFileSync(concernsPath, `# Concerns\n\nGenerated: ${timestamp()}\n\n## TODO\n- [ ] Identify technical debt\n- [ ] Flag security concerns\n- [ ] Note missing tests\n`, "utf-8");
|
|
91
|
+
// Domain model extraction
|
|
92
|
+
let domainHints = "";
|
|
93
|
+
try {
|
|
94
|
+
const types = execSync(`grep -rn 'export\\s\\+\\(interface\\|type\\|class\\)' --include='*.ts' --include='*.go' . 2>/dev/null | grep -v node_modules | grep -v dist | head -50`, { cwd, encoding: "utf-8" }).trim();
|
|
95
|
+
if (types)
|
|
96
|
+
domainHints += `## Types/Interfaces (potential entities)\n\`\`\`\n${types}\n\`\`\`\n\n`;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// no ts/go files
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const dirs = execSync(`find . -type d -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | sort`, { cwd, encoding: "utf-8" }).trim();
|
|
103
|
+
if (dirs)
|
|
104
|
+
domainHints += `## Directory Structure (potential bounded contexts)\n\`\`\`\n${dirs}\n\`\`\`\n`;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
const hintsPath = path.join(outDir, "DOMAIN-HINTS.md");
|
|
110
|
+
fs.writeFileSync(hintsPath, `# 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`, "utf-8");
|
|
111
|
+
return [stackPath, archPath, convPath, concernsPath, hintsPath];
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../../src/gsd/domain.ts"],"names":[],"mappings":"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","sourcesContent":["// GSD Domain module — domain model and codebase mapping operations.\n// Part of the draht GSD (Get Shit Done) methodology.\n// Exported via src/gsd/index.ts and @draht/coding-agent.\n\nimport { execSync } from \"node:child_process\";\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 timestamp(): string {\n\treturn new Date().toISOString().replace(\"T\", \" \").slice(0, 19);\n}\n\n/**\n * Generate a DOMAIN-MODEL.md scaffold from PROJECT.md.\n * Requires .planning/PROJECT.md to exist.\n * Returns the path to the created file.\n */\nexport function createDomainModel(cwd: string): string {\n\tconst projectPath = planningPath(cwd, \"PROJECT.md\");\n\tif (!fs.existsSync(projectPath)) {\n\t\tthrow new Error(\"No PROJECT.md found — run create-project first\");\n\t}\n\n\tconst outPath = planningPath(cwd, \"DOMAIN-MODEL.md\");\n\tconst 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`;\n\tfs.writeFileSync(outPath, tmpl, \"utf-8\");\n\treturn outPath;\n}\n\n/**\n * Scan the codebase and write .planning/codebase/ analysis files.\n * Returns array of created file paths.\n */\nexport function mapCodebase(cwd: string): string[] {\n\tconst outDir = planningPath(cwd, \"codebase\");\n\tensureDir(outDir);\n\n\t// Gather file tree\n\tlet tree = \"\";\n\ttry {\n\t\ttree = execSync(\n\t\t\t`find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.planning/*' | head -200`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t);\n\t} catch {\n\t\ttree = \"(unable to list files)\";\n\t}\n\n\t// Gather package info\n\tlet pkgJson: {\n\t\tname?: string;\n\t\tdependencies?: Record<string, string>;\n\t\tdevDependencies?: Record<string, string>;\n\t} | null = null;\n\ttry {\n\t\tpkgJson = JSON.parse(fs.readFileSync(path.join(cwd, \"package.json\"), \"utf-8\"));\n\t} catch {\n\t\t// not a Node.js project\n\t}\n\n\tconst stackPath = path.join(outDir, \"STACK.md\");\n\tfs.writeFileSync(\n\t\tstackPath,\n\t\t`# 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`,\n\t\t\"utf-8\",\n\t);\n\n\tconst archPath = path.join(outDir, \"ARCHITECTURE.md\");\n\tfs.writeFileSync(\n\t\tarchPath,\n\t\t`# Architecture\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Document file/directory patterns\\n- [ ] Map module boundaries\\n- [ ] Describe data flow\\n`,\n\t\t\"utf-8\",\n\t);\n\n\tconst convPath = path.join(outDir, \"CONVENTIONS.md\");\n\tfs.writeFileSync(\n\t\tconvPath,\n\t\t`# Conventions\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Document code style patterns\\n- [ ] Document testing patterns\\n- [ ] Document error handling approach\\n`,\n\t\t\"utf-8\",\n\t);\n\n\tconst concernsPath = path.join(outDir, \"CONCERNS.md\");\n\tfs.writeFileSync(\n\t\tconcernsPath,\n\t\t`# Concerns\\n\\nGenerated: ${timestamp()}\\n\\n## TODO\\n- [ ] Identify technical debt\\n- [ ] Flag security concerns\\n- [ ] Note missing tests\\n`,\n\t\t\"utf-8\",\n\t);\n\n\t// Domain model extraction\n\tlet domainHints = \"\";\n\ttry {\n\t\tconst types = execSync(\n\t\t\t`grep -rn 'export\\\\s\\\\+\\\\(interface\\\\|type\\\\|class\\\\)' --include='*.ts' --include='*.go' . 2>/dev/null | grep -v node_modules | grep -v dist | head -50`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t).trim();\n\t\tif (types) domainHints += `## Types/Interfaces (potential entities)\\n\\`\\`\\`\\n${types}\\n\\`\\`\\`\\n\\n`;\n\t} catch {\n\t\t// no ts/go files\n\t}\n\ttry {\n\t\tconst dirs = execSync(\n\t\t\t`find . -type d -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' | sort`,\n\t\t\t{ cwd, encoding: \"utf-8\" },\n\t\t).trim();\n\t\tif (dirs) domainHints += `## Directory Structure (potential bounded contexts)\\n\\`\\`\\`\\n${dirs}\\n\\`\\`\\`\\n`;\n\t} catch {\n\t\t// ignore\n\t}\n\n\tconst hintsPath = path.join(outDir, \"DOMAIN-HINTS.md\");\n\tfs.writeFileSync(\n\t\thintsPath,\n\t\t`# 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`,\n\t\t\"utf-8\",\n\t);\n\n\treturn [stackPath, archPath, convPath, concernsPath, hintsPath];\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CommitResult {
|
|
2
|
+
hash: string | null;
|
|
3
|
+
tddWarning: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if any file in the list matches known test file patterns.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hasTestFiles(files: string[]): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Stage all changes and commit as a task in the GSD methodology.
|
|
11
|
+
* Message format: feat(NN-NN): description
|
|
12
|
+
* Sets tddWarning=true when no test files are in the commit.
|
|
13
|
+
*/
|
|
14
|
+
export declare function commitTask(cwd: string, phaseNum: number, planNum: number, description: string): CommitResult;
|
|
15
|
+
/**
|
|
16
|
+
* Stage all changes and commit as a docs update.
|
|
17
|
+
* Message format: docs: message
|
|
18
|
+
*/
|
|
19
|
+
export declare function commitDocs(cwd: string, message: string): CommitResult;
|
|
20
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/gsd/git.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,YAAY,CAwB5G;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAUrE","sourcesContent":["// GSD Git module — git commit operations for the GSD lifecycle.\n// Part of the draht GSD (Get Shit Done) methodology.\n// Exported via src/gsd/index.ts and @draht/coding-agent.\n\nimport { execSync } from \"node:child_process\";\n\nexport interface CommitResult {\n\thash: string | null;\n\ttddWarning: boolean;\n}\n\n/**\n * Returns true if any file in the list matches known test file patterns.\n */\nexport function hasTestFiles(files: string[]): boolean {\n\treturn files.some((f) => /\\.(test|spec)\\.(ts|tsx|js|jsx)$|_test\\.(go|ts)$/.test(f));\n}\n\n/**\n * Stage all changes and commit as a task in the GSD methodology.\n * Message format: feat(NN-NN): description\n * Sets tddWarning=true when no test files are in the commit.\n */\nexport function commitTask(cwd: string, phaseNum: number, planNum: number, description: string): CommitResult {\n\tconst scope = `${String(phaseNum).padStart(2, \"0\")}-${String(planNum).padStart(2, \"0\")}`;\n\tconst message = `feat(${scope}): ${description}`;\n\ttry {\n\t\texecSync(\"git add -A\", { cwd, stdio: \"pipe\" });\n\t\texecSync(`git commit -m ${JSON.stringify(message)}`, { cwd, stdio: \"pipe\" });\n\t\tconst hash = execSync(\"git rev-parse HEAD\", { cwd, encoding: \"utf-8\" }).trim();\n\t\tlet tddWarning = false;\n\t\ttry {\n\t\t\tconst files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {\n\t\t\t\tcwd,\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t})\n\t\t\t\t.trim()\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter(Boolean);\n\t\t\ttddWarning = !hasTestFiles(files);\n\t\t} catch {\n\t\t\t// not a git repo or commit not found\n\t\t}\n\t\treturn { hash, tddWarning };\n\t} catch {\n\t\treturn { hash: null, tddWarning: false };\n\t}\n}\n\n/**\n * Stage all changes and commit as a docs update.\n * Message format: docs: message\n */\nexport function commitDocs(cwd: string, message: string): CommitResult {\n\tconst msg = `docs: ${message}`;\n\ttry {\n\t\texecSync(\"git add -A\", { cwd, stdio: \"pipe\" });\n\t\texecSync(`git commit -m ${JSON.stringify(msg)}`, { cwd, stdio: \"pipe\" });\n\t\tconst hash = execSync(\"git rev-parse HEAD\", { cwd, encoding: \"utf-8\" }).trim();\n\t\treturn { hash, tddWarning: false };\n\t} catch {\n\t\treturn { hash: null, tddWarning: false };\n\t}\n}\n"]}
|
package/dist/gsd/git.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// GSD Git module — git commit operations for the GSD lifecycle.
|
|
2
|
+
// Part of the draht GSD (Get Shit Done) methodology.
|
|
3
|
+
// Exported via src/gsd/index.ts and @draht/coding-agent.
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
/**
|
|
6
|
+
* Returns true if any file in the list matches known test file patterns.
|
|
7
|
+
*/
|
|
8
|
+
export function hasTestFiles(files) {
|
|
9
|
+
return files.some((f) => /\.(test|spec)\.(ts|tsx|js|jsx)$|_test\.(go|ts)$/.test(f));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Stage all changes and commit as a task in the GSD methodology.
|
|
13
|
+
* Message format: feat(NN-NN): description
|
|
14
|
+
* Sets tddWarning=true when no test files are in the commit.
|
|
15
|
+
*/
|
|
16
|
+
export function commitTask(cwd, phaseNum, planNum, description) {
|
|
17
|
+
const scope = `${String(phaseNum).padStart(2, "0")}-${String(planNum).padStart(2, "0")}`;
|
|
18
|
+
const message = `feat(${scope}): ${description}`;
|
|
19
|
+
try {
|
|
20
|
+
execSync("git add -A", { cwd, stdio: "pipe" });
|
|
21
|
+
execSync(`git commit -m ${JSON.stringify(message)}`, { cwd, stdio: "pipe" });
|
|
22
|
+
const hash = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
23
|
+
let tddWarning = false;
|
|
24
|
+
try {
|
|
25
|
+
const files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
})
|
|
29
|
+
.trim()
|
|
30
|
+
.split("\n")
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
tddWarning = !hasTestFiles(files);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// not a git repo or commit not found
|
|
36
|
+
}
|
|
37
|
+
return { hash, tddWarning };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { hash: null, tddWarning: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stage all changes and commit as a docs update.
|
|
45
|
+
* Message format: docs: message
|
|
46
|
+
*/
|
|
47
|
+
export function commitDocs(cwd, message) {
|
|
48
|
+
const msg = `docs: ${message}`;
|
|
49
|
+
try {
|
|
50
|
+
execSync("git add -A", { cwd, stdio: "pipe" });
|
|
51
|
+
execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd, stdio: "pipe" });
|
|
52
|
+
const hash = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
|
|
53
|
+
return { hash, tddWarning: false };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { hash: null, tddWarning: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/gsd/git.ts"],"names":[],"mappings":"AAAA,kEAAgE;AAChE,qDAAqD;AACrD,yDAAyD;AAEzD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAO9C;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAe,EAAW;IACtD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iDAAiD,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAAA,CACpF;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAe,EAAE,WAAmB,EAAgB;IAC7G,MAAM,KAAK,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACzF,MAAM,OAAO,GAAG,QAAQ,KAAK,MAAM,WAAW,EAAE,CAAC;IACjD,IAAI,CAAC;QACJ,QAAQ,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC7E,MAAM,IAAI,GAAG,QAAQ,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC;YACJ,MAAM,KAAK,GAAG,QAAQ,CAAC,+CAA+C,IAAI,EAAE,EAAE;gBAC7E,GAAG;gBACH,QAAQ,EAAE,OAAO;aACjB,CAAC;iBACA,IAAI,EAAE;iBACN,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,OAAO,CAAC,CAAC;YAClB,UAAU,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACR,qCAAqC;QACtC,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC1C,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,OAAe,EAAgB;IACtE,MAAM,GAAG,GAAG,SAAS,OAAO,EAAE,CAAC;IAC/B,IAAI,CAAC;QACJ,QAAQ,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,iBAAiB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QACzE,MAAM,IAAI,GAAG,QAAQ,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC1C,CAAC;AAAA,CACD","sourcesContent":["// GSD Git module — git commit operations for the GSD lifecycle.\n// Part of the draht GSD (Get Shit Done) methodology.\n// Exported via src/gsd/index.ts and @draht/coding-agent.\n\nimport { execSync } from \"node:child_process\";\n\nexport interface CommitResult {\n\thash: string | null;\n\ttddWarning: boolean;\n}\n\n/**\n * Returns true if any file in the list matches known test file patterns.\n */\nexport function hasTestFiles(files: string[]): boolean {\n\treturn files.some((f) => /\\.(test|spec)\\.(ts|tsx|js|jsx)$|_test\\.(go|ts)$/.test(f));\n}\n\n/**\n * Stage all changes and commit as a task in the GSD methodology.\n * Message format: feat(NN-NN): description\n * Sets tddWarning=true when no test files are in the commit.\n */\nexport function commitTask(cwd: string, phaseNum: number, planNum: number, description: string): CommitResult {\n\tconst scope = `${String(phaseNum).padStart(2, \"0\")}-${String(planNum).padStart(2, \"0\")}`;\n\tconst message = `feat(${scope}): ${description}`;\n\ttry {\n\t\texecSync(\"git add -A\", { cwd, stdio: \"pipe\" });\n\t\texecSync(`git commit -m ${JSON.stringify(message)}`, { cwd, stdio: \"pipe\" });\n\t\tconst hash = execSync(\"git rev-parse HEAD\", { cwd, encoding: \"utf-8\" }).trim();\n\t\tlet tddWarning = false;\n\t\ttry {\n\t\t\tconst files = execSync(`git diff-tree --no-commit-id --name-only -r ${hash}`, {\n\t\t\t\tcwd,\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t})\n\t\t\t\t.trim()\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter(Boolean);\n\t\t\ttddWarning = !hasTestFiles(files);\n\t\t} catch {\n\t\t\t// not a git repo or commit not found\n\t\t}\n\t\treturn { hash, tddWarning };\n\t} catch {\n\t\treturn { hash: null, tddWarning: false };\n\t}\n}\n\n/**\n * Stage all changes and commit as a docs update.\n * Message format: docs: message\n */\nexport function commitDocs(cwd: string, message: string): CommitResult {\n\tconst msg = `docs: ${message}`;\n\ttry {\n\t\texecSync(\"git add -A\", { cwd, stdio: \"pipe\" });\n\t\texecSync(`git commit -m ${JSON.stringify(msg)}`, { cwd, stdio: \"pipe\" });\n\t\tconst hash = execSync(\"git rev-parse HEAD\", { cwd, encoding: \"utf-8\" }).trim();\n\t\treturn { hash, tddWarning: false };\n\t} catch {\n\t\treturn { hash: null, tddWarning: false };\n\t}\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ToolchainInfo {
|
|
2
|
+
pm: "npm" | "bun" | "pnpm" | "yarn";
|
|
3
|
+
testCmd: string;
|
|
4
|
+
coverageCmd: string;
|
|
5
|
+
lintCmd: string;
|
|
6
|
+
}
|
|
7
|
+
export interface HookConfig {
|
|
8
|
+
coverageThreshold: number;
|
|
9
|
+
tddMode: "strict" | "advisory";
|
|
10
|
+
qualityGateStrict: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Detect package manager from lockfiles and package.json scripts.
|
|
14
|
+
* Priority: bun.lockb/bun.lock > pnpm-lock.yaml > yarn.lock > package-lock.json > fallback npm
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectToolchain(cwd: string): ToolchainInfo;
|
|
17
|
+
/**
|
|
18
|
+
* Read hook configuration from .planning/config.json hooks section.
|
|
19
|
+
* Falls back to defaults on missing file or parse errors.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readHookConfig(cwd: string): HookConfig;
|
|
22
|
+
//# sourceMappingURL=hook-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-utils.d.ts","sourceRoot":"","sources":["../../src/gsd/hook-utils.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;CAC3B;AAQD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAgE1D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAyBtD","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"]}
|