@draht/coding-agent 2026.3.4 → 2026.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/core/prompt-templates.d.ts.map +1 -1
  3. package/dist/core/prompt-templates.js +4 -2
  4. package/dist/core/prompt-templates.js.map +1 -1
  5. package/dist/gsd/domain-validator.d.ts +18 -0
  6. package/dist/gsd/domain-validator.d.ts.map +1 -0
  7. package/dist/gsd/domain-validator.js +61 -0
  8. package/dist/gsd/domain-validator.js.map +1 -0
  9. package/dist/gsd/domain.d.ts +12 -0
  10. package/dist/gsd/domain.d.ts.map +1 -0
  11. package/dist/gsd/domain.js +113 -0
  12. package/dist/gsd/domain.js.map +1 -0
  13. package/dist/gsd/git.d.ts +20 -0
  14. package/dist/gsd/git.d.ts.map +1 -0
  15. package/dist/gsd/git.js +59 -0
  16. package/dist/gsd/git.js.map +1 -0
  17. package/dist/gsd/hook-utils.d.ts +22 -0
  18. package/dist/gsd/hook-utils.d.ts.map +1 -0
  19. package/dist/gsd/hook-utils.js +100 -0
  20. package/dist/gsd/hook-utils.js.map +1 -0
  21. package/dist/gsd/index.d.ts +9 -0
  22. package/dist/gsd/index.d.ts.map +1 -0
  23. package/dist/gsd/index.js +8 -0
  24. package/dist/gsd/index.js.map +1 -0
  25. package/dist/gsd/planning.d.ts +20 -0
  26. package/dist/gsd/planning.d.ts.map +1 -0
  27. package/dist/gsd/planning.js +167 -0
  28. package/dist/gsd/planning.js.map +1 -0
  29. package/dist/hooks/gsd/draht-post-task.js +44 -11
  30. package/dist/hooks/gsd/draht-quality-gate.js +99 -57
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  36. package/dist/modes/interactive/interactive-mode.js +2 -2
  37. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  38. package/dist/prompts/agents/build.md +5 -1
  39. package/dist/prompts/agents/plan.md +5 -1
  40. package/dist/prompts/agents/verify.md +5 -1
  41. package/dist/prompts/commands/atomic-commit.md +8 -16
  42. package/dist/prompts/commands/discuss-phase.md +9 -3
  43. package/dist/prompts/commands/execute-phase.md +15 -8
  44. package/dist/prompts/commands/fix.md +6 -0
  45. package/dist/prompts/commands/init-project.md +9 -3
  46. package/dist/prompts/commands/map-codebase.md +7 -1
  47. package/dist/prompts/commands/new-project.md +8 -2
  48. package/dist/prompts/commands/next-milestone.md +4 -0
  49. package/dist/prompts/commands/pause-work.md +4 -0
  50. package/dist/prompts/commands/plan-phase.md +11 -5
  51. package/dist/prompts/commands/progress.md +4 -0
  52. package/dist/prompts/commands/quick.md +8 -2
  53. package/dist/prompts/commands/resume-work.md +4 -0
  54. package/dist/prompts/commands/review.md +6 -0
  55. package/dist/prompts/commands/verify-work.md +10 -4
  56. package/hooks/gsd/draht-post-task.js +44 -11
  57. package/hooks/gsd/draht-quality-gate.js +99 -57
  58. package/package.json +5 -5
  59. package/prompts/agents/build.md +5 -1
  60. package/prompts/agents/plan.md +5 -1
  61. package/prompts/agents/verify.md +5 -1
  62. package/prompts/commands/atomic-commit.md +8 -16
  63. package/prompts/commands/discuss-phase.md +9 -3
  64. package/prompts/commands/execute-phase.md +15 -8
  65. package/prompts/commands/fix.md +6 -0
  66. package/prompts/commands/init-project.md +9 -3
  67. package/prompts/commands/map-codebase.md +7 -1
  68. package/prompts/commands/new-project.md +8 -2
  69. package/prompts/commands/next-milestone.md +4 -0
  70. package/prompts/commands/pause-work.md +4 -0
  71. package/prompts/commands/plan-phase.md +11 -5
  72. package/prompts/commands/progress.md +4 -0
  73. package/prompts/commands/quick.md +8 -2
  74. package/prompts/commands/resume-work.md +4 -0
  75. package/prompts/commands/review.md +6 -0
  76. package/prompts/commands/verify-work.md +10 -4
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
- description = description ? `${description} ${sourceLabel}` : sourceLabel;
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"]}
@@ -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"]}