gsd-pi 2.8.1 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loader.js +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/bash.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/packages/pi-coding-agent/src/index.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/gsd/auto.ts +18 -36
- package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
- package/src/resources/extensions/gsd/gitignore.ts +27 -0
- package/src/resources/extensions/gsd/guided-flow.ts +10 -9
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
- package/src/resources/extensions/gsd/tests/git-service.test.ts +53 -1
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
|
@@ -9,6 +9,13 @@ export declare function getShellConfig(): {
|
|
|
9
9
|
shell: string;
|
|
10
10
|
args: string[];
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
14
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
15
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
16
|
+
* No-op on non-Windows platforms.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sanitizeCommand(command: string): string;
|
|
12
19
|
export declare function getShellEnv(): NodeJS.ProcessEnv;
|
|
13
20
|
/**
|
|
14
21
|
* Sanitize binary output for display/storage.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AA2CA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAoElE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAwBjD"}
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AA2CA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAoElE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAwBjD"}
|
|
@@ -102,6 +102,17 @@ export function getShellConfig() {
|
|
|
102
102
|
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
103
103
|
return cachedShellConfig;
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
107
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
108
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
109
|
+
* No-op on non-Windows platforms.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeCommand(command) {
|
|
112
|
+
if (process.platform !== "win32")
|
|
113
|
+
return command;
|
|
114
|
+
return command.replace(/(\d*>>?) *\bNUL\b(?=\s|;|\||&|\)|$)/gi, "$1 /dev/null");
|
|
115
|
+
}
|
|
105
116
|
export function getShellEnv() {
|
|
106
117
|
const binDir = getBinDir();
|
|
107
118
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir, getSettingsPath } from \"../config.js\";\nimport { SettingsManager } from \"../core/settings-manager.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = SettingsManager.create();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\nPlease update shellPath in ${getSettingsPath()}`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ${getSettingsPath()}\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\tcachedShellConfig = { shell: \"/bin/bash\", args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir, getSettingsPath } from \"../config.js\";\nimport { SettingsManager } from \"../core/settings-manager.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = SettingsManager.create();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\nPlease update shellPath in ${getSettingsPath()}`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ${getSettingsPath()}\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\tcachedShellConfig = { shell: \"/bin/bash\", args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\n/**\n * On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.\n * Git Bash doesn't recognize NUL as a device name and creates a literal file\n * that is undeletable due to NUL being a reserved Windows device name.\n * No-op on non-Windows platforms.\n */\nexport function sanitizeCommand(command: string): string {\n\tif (process.platform !== \"win32\") return command;\n\treturn command.replace(/(\\d*>>?) *\\bNUL\\b(?=\\s|;|\\||&|\\)|$)/gi, \"$1 /dev/null\");\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -12,7 +12,7 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { type ChildProcess, spawn } from "child_process";
|
|
14
14
|
import stripAnsi from "strip-ansi";
|
|
15
|
-
import { getShellConfig, getShellEnv, killProcessTree, sanitizeBinaryOutput } from "../utils/shell.js";
|
|
15
|
+
import { getShellConfig, getShellEnv, killProcessTree, sanitizeBinaryOutput, sanitizeCommand } from "../utils/shell.js";
|
|
16
16
|
import type { BashOperations } from "./tools/bash.js";
|
|
17
17
|
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
|
18
18
|
|
|
@@ -61,7 +61,7 @@ export interface BashResult {
|
|
|
61
61
|
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
62
62
|
return new Promise((resolve, reject) => {
|
|
63
63
|
const { shell, args } = getShellConfig();
|
|
64
|
-
const child: ChildProcess = spawn(shell, [...args, command], {
|
|
64
|
+
const child: ChildProcess = spawn(shell, [...args, sanitizeCommand(command)], {
|
|
65
65
|
detached: true,
|
|
66
66
|
env: getShellEnv(),
|
|
67
67
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import type { AgentTool } from "@gsd/pi-agent-core";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import { spawn } from "child_process";
|
|
9
|
-
import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
|
|
9
|
+
import { getShellConfig, getShellEnv, killProcessTree, sanitizeCommand } from "../../utils/shell.js";
|
|
10
10
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
|
|
11
11
|
import type { ArtifactManager } from "../artifact-manager.js";
|
|
12
12
|
|
|
@@ -211,7 +211,7 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
|
|
|
211
211
|
onUpdate?,
|
|
212
212
|
) => {
|
|
213
213
|
// Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
|
|
214
|
-
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
|
214
|
+
const resolvedCommand = sanitizeCommand(commandPrefix ? `${commandPrefix}\n${command}` : command);
|
|
215
215
|
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
|
216
216
|
|
|
217
217
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, mock, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolveToCwd, expandPath } from "./path-utils.js";
|
|
4
|
+
|
|
5
|
+
describe("resolveToCwd", () => {
|
|
6
|
+
it("resolves relative paths against cwd", () => {
|
|
7
|
+
const result = resolveToCwd("foo/bar.txt", "/home/user/project");
|
|
8
|
+
assert.equal(result, "/home/user/project/foo/bar.txt");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns absolute paths unchanged", () => {
|
|
12
|
+
const result = resolveToCwd("/absolute/path.txt", "/home/user/project");
|
|
13
|
+
assert.equal(result, "/absolute/path.txt");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("expands ~ to home directory", () => {
|
|
17
|
+
const result = resolveToCwd("~/file.txt", "/home/user/project");
|
|
18
|
+
assert.ok(result.endsWith("/file.txt"));
|
|
19
|
+
assert.ok(!result.includes("~"));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("normalizeMsysPath (via resolveToCwd on win32)", () => {
|
|
24
|
+
const originalPlatform = process.platform;
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("converts /c/Users/... to C:\\Users\\... on win32", () => {
|
|
31
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
32
|
+
// Re-import to pick up platform change — but since normalizeMsysPath
|
|
33
|
+
// reads process.platform at call time, we can test directly.
|
|
34
|
+
// On non-Windows, resolveToCwd treats /c/Users as absolute, so we
|
|
35
|
+
// test the normalization logic by checking the MSYS regex behavior.
|
|
36
|
+
const msysPath = "/c/Users/test/project";
|
|
37
|
+
const msysRegex = /^\/[a-zA-Z]\//;
|
|
38
|
+
assert.ok(msysRegex.test(msysPath), "MSYS path pattern matches");
|
|
39
|
+
|
|
40
|
+
// Simulate the conversion
|
|
41
|
+
const converted = `${msysPath[1].toUpperCase()}:\\${msysPath.slice(3).replace(/\//g, "\\")}`;
|
|
42
|
+
assert.equal(converted, "C:\\Users\\test\\project");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("converts /f/Projects to F:\\Projects on win32", () => {
|
|
46
|
+
const msysPath = "/f/Projects";
|
|
47
|
+
const converted = `${msysPath[1].toUpperCase()}:\\${msysPath.slice(3).replace(/\//g, "\\")}`;
|
|
48
|
+
assert.equal(converted, "F:\\Projects");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not convert regular Unix paths", () => {
|
|
52
|
+
const regularPath = "/usr/local/bin";
|
|
53
|
+
const msysRegex = /^\/[a-zA-Z]\//;
|
|
54
|
+
// /u/local/bin would match, but /usr/local/bin has 3+ chars before /
|
|
55
|
+
// Actually /u/ would match — but /usr/ won't because 'us' is 2 chars.
|
|
56
|
+
// The regex checks single letter after leading slash.
|
|
57
|
+
assert.ok(!msysRegex.test("/usr/local/bin"), "/usr/... is not an MSYS path");
|
|
58
|
+
assert.ok(msysRegex.test("/u/local/bin"), "/u/... would match (single letter)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not convert paths without leading slash", () => {
|
|
62
|
+
const msysRegex = /^\/[a-zA-Z]\//;
|
|
63
|
+
assert.ok(!msysRegex.test("c/Users/test"), "no leading slash — not MSYS");
|
|
64
|
+
assert.ok(!msysRegex.test("relative/path"), "relative path — not MSYS");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -47,12 +47,24 @@ export function expandPath(filePath: string): string {
|
|
|
47
47
|
return normalized;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* On Windows, convert MSYS/MinGW-style paths (/c/Users/...) to native
|
|
52
|
+
* drive letter paths (C:\Users\...). LLMs often produce these when given
|
|
53
|
+
* Windows paths in prompts.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeMsysPath(p: string): string {
|
|
56
|
+
if (process.platform === "win32" && /^\/[a-zA-Z]\//.test(p)) {
|
|
57
|
+
return `${p[1]!.toUpperCase()}:\\${p.slice(3).replace(/\//g, "\\")}`;
|
|
58
|
+
}
|
|
59
|
+
return p;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
/**
|
|
51
63
|
* Resolve a path relative to the given cwd.
|
|
52
|
-
* Handles ~ expansion and absolute paths.
|
|
64
|
+
* Handles ~ expansion, MSYS-style paths on Windows, and absolute paths.
|
|
53
65
|
*/
|
|
54
66
|
export function resolveToCwd(filePath: string, cwd: string): string {
|
|
55
|
-
const expanded = expandPath(filePath);
|
|
67
|
+
const expanded = normalizeMsysPath(expandPath(filePath));
|
|
56
68
|
if (isAbsolute(expanded)) {
|
|
57
69
|
return expanded;
|
|
58
70
|
}
|
|
@@ -333,4 +333,4 @@ export {
|
|
|
333
333
|
export { copyToClipboard } from "./utils/clipboard.js";
|
|
334
334
|
export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
|
|
335
335
|
// Shell utilities
|
|
336
|
-
export { getShellConfig } from "./utils/shell.js";
|
|
336
|
+
export { getShellConfig, sanitizeCommand } from "./utils/shell.js";
|
|
@@ -824,10 +824,12 @@ export class InteractiveMode {
|
|
|
824
824
|
|
|
825
825
|
// Try parent directories (package manager stores directory paths)
|
|
826
826
|
let current = p;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
827
|
+
let parent = path.dirname(current);
|
|
828
|
+
while (parent !== current) {
|
|
829
|
+
const meta = metadata.get(parent);
|
|
830
|
+
if (meta) return meta;
|
|
831
|
+
current = parent;
|
|
832
|
+
parent = path.dirname(current);
|
|
831
833
|
}
|
|
832
834
|
|
|
833
835
|
return undefined;
|
|
@@ -118,6 +118,17 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
|
|
118
118
|
return cachedShellConfig;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
123
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
124
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
125
|
+
* No-op on non-Windows platforms.
|
|
126
|
+
*/
|
|
127
|
+
export function sanitizeCommand(command: string): string {
|
|
128
|
+
if (process.platform !== "win32") return command;
|
|
129
|
+
return command.replace(/(\d*>>?) *\bNUL\b(?=\s|;|\||&|\)|$)/gi, "$1 /dev/null");
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
export function getShellEnv(): NodeJS.ProcessEnv {
|
|
122
133
|
const binDir = getBinDir();
|
|
123
134
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
DEFAULT_MAX_BYTES,
|
|
35
35
|
DEFAULT_MAX_LINES,
|
|
36
36
|
getShellConfig,
|
|
37
|
+
sanitizeCommand,
|
|
37
38
|
} from "@gsd/pi-coding-agent";
|
|
38
39
|
import {
|
|
39
40
|
Text,
|
|
@@ -582,7 +583,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
582
583
|
const env = { ...process.env, ...(opts.env || {}) };
|
|
583
584
|
|
|
584
585
|
const { shell, args: shellArgs } = getShellConfig();
|
|
585
|
-
const proc = spawn(shell, [...shellArgs, opts.command], {
|
|
586
|
+
const proc = spawn(shell, [...shellArgs, sanitizeCommand(opts.command)], {
|
|
586
587
|
cwd: opts.cwd,
|
|
587
588
|
stdio: ["pipe", "pipe", "pipe"],
|
|
588
589
|
env,
|
|
@@ -48,14 +48,14 @@ import {
|
|
|
48
48
|
validateCompleteBoundary,
|
|
49
49
|
formatValidationIssues,
|
|
50
50
|
} from "./observability-validator.js";
|
|
51
|
-
import { ensureGitignore } from "./gitignore.js";
|
|
51
|
+
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
52
52
|
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
53
53
|
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
54
54
|
import {
|
|
55
55
|
initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
|
|
56
56
|
getProjectTotals, formatCost, formatTokenCount,
|
|
57
57
|
} from "./metrics.js";
|
|
58
|
-
import { join } from "node:path";
|
|
58
|
+
import { dirname, join } from "node:path";
|
|
59
59
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
60
60
|
import { execSync, execFileSync } from "node:child_process";
|
|
61
61
|
import {
|
|
@@ -381,6 +381,7 @@ export async function startAuto(
|
|
|
381
381
|
|
|
382
382
|
// Ensure .gitignore has baseline patterns
|
|
383
383
|
ensureGitignore(base);
|
|
384
|
+
untrackRuntimeFiles(base);
|
|
384
385
|
|
|
385
386
|
// Bootstrap .gsd/ if it doesn't exist
|
|
386
387
|
const gsdDir = join(base, ".gsd");
|
|
@@ -1696,13 +1697,11 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
|
|
|
1696
1697
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1697
1698
|
|
|
1698
1699
|
const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
|
|
1699
|
-
const outputAbsPath = resolveMilestoneFile(base, mid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1700
1700
|
return loadPrompt("research-milestone", {
|
|
1701
1701
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
1702
1702
|
milestonePath: relMilestonePath(base, mid),
|
|
1703
1703
|
contextPath: contextRel,
|
|
1704
1704
|
outputPath: outputRelPath,
|
|
1705
|
-
outputAbsPath,
|
|
1706
1705
|
inlinedContext,
|
|
1707
1706
|
...buildSkillDiscoveryVars(),
|
|
1708
1707
|
});
|
|
@@ -1730,7 +1729,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
1730
1729
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1731
1730
|
|
|
1732
1731
|
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
1733
|
-
const outputAbsPath = resolveMilestoneFile(base, mid, "ROADMAP") ?? join(base, outputRelPath);
|
|
1734
1732
|
const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
|
|
1735
1733
|
return loadPrompt("plan-milestone", {
|
|
1736
1734
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
@@ -1738,7 +1736,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
1738
1736
|
contextPath: contextRel,
|
|
1739
1737
|
researchPath: researchRel,
|
|
1740
1738
|
outputPath: outputRelPath,
|
|
1741
|
-
outputAbsPath,
|
|
1742
1739
|
secretsOutputPath,
|
|
1743
1740
|
inlinedContext,
|
|
1744
1741
|
});
|
|
@@ -1770,7 +1767,6 @@ async function buildResearchSlicePrompt(
|
|
|
1770
1767
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1771
1768
|
|
|
1772
1769
|
const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
|
|
1773
|
-
const outputAbsPath = resolveSliceFile(base, mid, sid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1774
1770
|
return loadPrompt("research-slice", {
|
|
1775
1771
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1776
1772
|
slicePath: relSlicePath(base, mid, sid),
|
|
@@ -1778,7 +1774,6 @@ async function buildResearchSlicePrompt(
|
|
|
1778
1774
|
contextPath: contextRel,
|
|
1779
1775
|
milestoneResearchPath: milestoneResearchRel,
|
|
1780
1776
|
outputPath: outputRelPath,
|
|
1781
|
-
outputAbsPath,
|
|
1782
1777
|
inlinedContext,
|
|
1783
1778
|
dependencySummaries: depContent,
|
|
1784
1779
|
...buildSkillDiscoveryVars(),
|
|
@@ -1807,16 +1802,12 @@ async function buildPlanSlicePrompt(
|
|
|
1807
1802
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1808
1803
|
|
|
1809
1804
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
1810
|
-
const outputAbsPath = resolveSliceFile(base, mid, sid, "PLAN") ?? join(base, outputRelPath);
|
|
1811
|
-
const sliceAbsPath = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
|
|
1812
1805
|
return loadPrompt("plan-slice", {
|
|
1813
1806
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1814
1807
|
slicePath: relSlicePath(base, mid, sid),
|
|
1815
|
-
sliceAbsPath,
|
|
1816
1808
|
roadmapPath: roadmapRel,
|
|
1817
1809
|
researchPath: researchRel,
|
|
1818
1810
|
outputPath: outputRelPath,
|
|
1819
|
-
outputAbsPath,
|
|
1820
1811
|
inlinedContext,
|
|
1821
1812
|
dependencySummaries: depContent,
|
|
1822
1813
|
});
|
|
@@ -1867,8 +1858,7 @@ async function buildExecuteTaskPrompt(
|
|
|
1867
1858
|
|
|
1868
1859
|
const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
|
|
1869
1860
|
|
|
1870
|
-
const
|
|
1871
|
-
const taskSummaryAbsPath = join(sliceDirAbs, "tasks", `${tid}-SUMMARY.md`);
|
|
1861
|
+
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
|
1872
1862
|
|
|
1873
1863
|
return loadPrompt("execute-task", {
|
|
1874
1864
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
|
|
@@ -1880,7 +1870,7 @@ async function buildExecuteTaskPrompt(
|
|
|
1880
1870
|
carryForwardSection,
|
|
1881
1871
|
resumeSection,
|
|
1882
1872
|
priorTaskLines: priorLines,
|
|
1883
|
-
|
|
1873
|
+
taskSummaryPath,
|
|
1884
1874
|
});
|
|
1885
1875
|
}
|
|
1886
1876
|
|
|
@@ -1916,17 +1906,17 @@ async function buildCompleteSlicePrompt(
|
|
|
1916
1906
|
|
|
1917
1907
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1918
1908
|
|
|
1919
|
-
const
|
|
1920
|
-
const
|
|
1921
|
-
const
|
|
1909
|
+
const sliceRel = relSlicePath(base, mid, sid);
|
|
1910
|
+
const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md`;
|
|
1911
|
+
const sliceUatPath = `${sliceRel}/${sid}-UAT.md`;
|
|
1922
1912
|
|
|
1923
1913
|
return loadPrompt("complete-slice", {
|
|
1924
1914
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1925
|
-
slicePath:
|
|
1915
|
+
slicePath: sliceRel,
|
|
1926
1916
|
roadmapPath: roadmapRel,
|
|
1927
1917
|
inlinedContext,
|
|
1928
|
-
|
|
1929
|
-
|
|
1918
|
+
sliceSummaryPath,
|
|
1919
|
+
sliceUatPath,
|
|
1930
1920
|
});
|
|
1931
1921
|
}
|
|
1932
1922
|
|
|
@@ -1965,15 +1955,14 @@ async function buildCompleteMilestonePrompt(
|
|
|
1965
1955
|
|
|
1966
1956
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1967
1957
|
|
|
1968
|
-
const
|
|
1969
|
-
const milestoneSummaryAbsPath = join(milestoneDirAbs, `${mid}-SUMMARY.md`);
|
|
1958
|
+
const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`;
|
|
1970
1959
|
|
|
1971
1960
|
return loadPrompt("complete-milestone", {
|
|
1972
1961
|
milestoneId: mid,
|
|
1973
1962
|
milestoneTitle: midTitle,
|
|
1974
1963
|
roadmapPath: roadmapRel,
|
|
1975
1964
|
inlinedContext,
|
|
1976
|
-
|
|
1965
|
+
milestoneSummaryPath,
|
|
1977
1966
|
});
|
|
1978
1967
|
}
|
|
1979
1968
|
|
|
@@ -2016,8 +2005,7 @@ async function buildReplanSlicePrompt(
|
|
|
2016
2005
|
|
|
2017
2006
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2018
2007
|
|
|
2019
|
-
const
|
|
2020
|
-
const replanAbsPath = join(sliceDirAbs, `${sid}-REPLAN.md`);
|
|
2008
|
+
const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`;
|
|
2021
2009
|
|
|
2022
2010
|
return loadPrompt("replan-slice", {
|
|
2023
2011
|
milestoneId: mid,
|
|
@@ -2027,7 +2015,7 @@ async function buildReplanSlicePrompt(
|
|
|
2027
2015
|
planPath: slicePlanRel,
|
|
2028
2016
|
blockerTaskId,
|
|
2029
2017
|
inlinedContext,
|
|
2030
|
-
|
|
2018
|
+
replanPath,
|
|
2031
2019
|
});
|
|
2032
2020
|
}
|
|
2033
2021
|
|
|
@@ -2145,8 +2133,6 @@ async function buildRunUatPrompt(
|
|
|
2145
2133
|
|
|
2146
2134
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2147
2135
|
|
|
2148
|
-
const sliceDirAbs = resolveSlicePath(base, mid, sliceId) ?? join(base, relSlicePath(base, mid, sliceId));
|
|
2149
|
-
const uatResultAbsPath = join(sliceDirAbs, `${sliceId}-UAT-RESULT.md`);
|
|
2150
2136
|
const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
|
|
2151
2137
|
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
2152
2138
|
|
|
@@ -2154,7 +2140,6 @@ async function buildRunUatPrompt(
|
|
|
2154
2140
|
milestoneId: mid,
|
|
2155
2141
|
sliceId,
|
|
2156
2142
|
uatPath,
|
|
2157
|
-
uatResultAbsPath,
|
|
2158
2143
|
uatResultPath,
|
|
2159
2144
|
uatType,
|
|
2160
2145
|
inlinedContext,
|
|
@@ -2181,9 +2166,7 @@ async function buildReassessRoadmapPrompt(
|
|
|
2181
2166
|
|
|
2182
2167
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2183
2168
|
|
|
2184
|
-
const
|
|
2185
|
-
const sliceDirAbs = resolveSlicePath(base, mid, completedSliceId) ?? join(base, relSlicePath(base, mid, completedSliceId));
|
|
2186
|
-
const assessmentAbsPath = join(sliceDirAbs, `${completedSliceId}-ASSESSMENT.md`);
|
|
2169
|
+
const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
|
|
2187
2170
|
|
|
2188
2171
|
return loadPrompt("reassess-roadmap", {
|
|
2189
2172
|
milestoneId: mid,
|
|
@@ -2191,8 +2174,7 @@ async function buildReassessRoadmapPrompt(
|
|
|
2191
2174
|
completedSliceId,
|
|
2192
2175
|
roadmapPath: roadmapRel,
|
|
2193
2176
|
completedSliceSummaryPath: summaryRel,
|
|
2194
|
-
assessmentPath
|
|
2195
|
-
assessmentAbsPath,
|
|
2177
|
+
assessmentPath,
|
|
2196
2178
|
inlinedContext,
|
|
2197
2179
|
});
|
|
2198
2180
|
}
|
|
@@ -2814,7 +2796,7 @@ function verifyExpectedArtifact(unitType: string, unitId: string, base: string):
|
|
|
2814
2796
|
export function writeBlockerPlaceholder(unitType: string, unitId: string, base: string, reason: string): string | null {
|
|
2815
2797
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
2816
2798
|
if (!absPath) return null;
|
|
2817
|
-
const dir =
|
|
2799
|
+
const dir = dirname(absPath);
|
|
2818
2800
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2819
2801
|
const content = [
|
|
2820
2802
|
`# BLOCKER — auto-mode recovery failed`,
|
|
@@ -13,6 +13,61 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## Semantics
|
|
17
|
+
|
|
18
|
+
### Empty Arrays vs Omitted Fields
|
|
19
|
+
|
|
20
|
+
**Empty arrays (`[]`) are equivalent to omitting the field entirely.** During validation, GSD deletes empty arrays from the preferences object (see `validatePreferences()` in `preferences.ts`):
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
for (const key of ["always_use_skills", "prefer_skills", "avoid_skills", "custom_instructions"] as const) {
|
|
24
|
+
if (validated[key] && validated[key]!.length === 0) {
|
|
25
|
+
delete validated[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
These are functionally identical:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# Explicit empty arrays — will be normalized away
|
|
34
|
+
prefer_skills: []
|
|
35
|
+
avoid_skills: []
|
|
36
|
+
skill_rules: []
|
|
37
|
+
|
|
38
|
+
# Omitted entirely — same result
|
|
39
|
+
# (just don't write these fields)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Recommendation:** Omit fields you don't need. Empty arrays add noise with no effect.
|
|
43
|
+
|
|
44
|
+
### Global vs Project Preferences
|
|
45
|
+
|
|
46
|
+
Preferences are loaded from two locations and merged:
|
|
47
|
+
|
|
48
|
+
1. **Global:** `~/.gsd/preferences.md` — applies to all projects
|
|
49
|
+
2. **Project:** `.gsd/preferences.md` — applies to the current project only
|
|
50
|
+
|
|
51
|
+
**Merge behavior** (see `mergePreferences()` in `preferences.ts`):
|
|
52
|
+
- **Scalar fields** (`skill_discovery`, `budget_ceiling`, etc.): Project wins if defined, otherwise global. Uses nullish coalescing (`??`).
|
|
53
|
+
- **Array fields** (`always_use_skills`, `prefer_skills`, etc.): Concatenated via `mergeStringLists()` (global first, then project).
|
|
54
|
+
- **Object fields** (`models`, `git`, `auto_supervisor`): Shallow merge via spread operator `{ ...base, ...override }`.
|
|
55
|
+
|
|
56
|
+
For `models`, project settings override global at the phase level. If global has `planning: opus` and project has `planning: sonnet`, the project wins. But if project omits `research`, global's `research` setting is preserved.
|
|
57
|
+
|
|
58
|
+
### Skill Discovery vs Skill Preferences
|
|
59
|
+
|
|
60
|
+
These are **separate concerns**:
|
|
61
|
+
|
|
62
|
+
| Field | What it controls | Code reference |
|
|
63
|
+
|-------|-----------------|----------------|
|
|
64
|
+
| `skill_discovery` | **Whether** GSD looks for relevant skills during research | `resolveSkillDiscoveryMode()` in `preferences.ts` |
|
|
65
|
+
| `always_use_skills`, `prefer_skills`, `avoid_skills` | **Which** skills to use when they're found relevant | `renderPreferencesForSystemPrompt()` in `preferences.ts` |
|
|
66
|
+
|
|
67
|
+
Setting `prefer_skills: []` does **not** disable skill discovery — it just means you have no preference overrides. Use `skill_discovery: off` to disable discovery entirely.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
16
71
|
## Field Guide
|
|
17
72
|
|
|
18
73
|
- `version`: schema version. Start at `1`.
|
|
@@ -60,6 +115,27 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
|
|
|
60
115
|
- Use `skill_rules` for situational routing, not broad personality preferences.
|
|
61
116
|
- Prefer skill names for stable built-in skills.
|
|
62
117
|
- Prefer absolute paths for local personal skills.
|
|
118
|
+
- **Omit fields you don't need** — empty arrays add noise with no effect.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Minimal Example
|
|
123
|
+
|
|
124
|
+
The cleanest preferences file only specifies what you actually want:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
---
|
|
128
|
+
version: 1
|
|
129
|
+
always_use_skills:
|
|
130
|
+
- debug-like-expert
|
|
131
|
+
skill_discovery: suggest
|
|
132
|
+
models:
|
|
133
|
+
planning: claude-opus-4-6
|
|
134
|
+
execution: claude-sonnet-4-6
|
|
135
|
+
---
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Everything else uses defaults. No `prefer_skills: []`, no `avoid_skills: []`, no `auto_supervisor: {}` — those are just noise.
|
|
63
139
|
|
|
64
140
|
---
|
|
65
141
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Patterns that are always correct regardless of project type.
|
|
@@ -106,6 +107,32 @@ export function ensureGitignore(basePath: string): boolean {
|
|
|
106
107
|
return true;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Remove BASELINE_PATTERNS runtime paths from the git index if they are
|
|
112
|
+
* currently tracked. This fixes repos that started tracking these files
|
|
113
|
+
* before the .gitignore rule was added — git continues tracking files
|
|
114
|
+
* already in the index even after .gitignore is updated.
|
|
115
|
+
*
|
|
116
|
+
* Only removes from the index (`--cached`), never from disk. Idempotent.
|
|
117
|
+
*/
|
|
118
|
+
export function untrackRuntimeFiles(basePath: string): void {
|
|
119
|
+
// The GSD runtime paths are the first 7 entries in BASELINE_PATTERNS
|
|
120
|
+
const runtimePaths = BASELINE_PATTERNS.slice(0, 7);
|
|
121
|
+
|
|
122
|
+
for (const pattern of runtimePaths) {
|
|
123
|
+
// Use -r for directory patterns (trailing slash), strip the slash for the command
|
|
124
|
+
const target = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
125
|
+
try {
|
|
126
|
+
execSync(`git rm -r --cached ${target}`, {
|
|
127
|
+
cwd: basePath,
|
|
128
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
// File not tracked or doesn't exist — expected, ignore
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
109
136
|
/**
|
|
110
137
|
* Ensure basePath/.gsd/PREFERENCES.md exists as an empty template.
|
|
111
138
|
* Creates the file with frontmatter only if it doesn't exist.
|