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.
Files changed (83) hide show
  1. package/dist/loader.js +5 -0
  2. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
  3. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  4. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +2 -2
  5. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +1 -1
  15. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  16. package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
  17. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  18. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  19. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  20. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  21. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  23. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
  25. package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
  26. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +2 -2
  27. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  28. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  29. package/node_modules/@gsd/pi-coding-agent/src/index.ts +1 -1
  30. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  31. package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
  32. package/package.json +1 -1
  33. package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
  34. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/tools/bash.js +2 -2
  36. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  38. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  40. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  42. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  43. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  44. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  45. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  46. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/index.js +1 -1
  48. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  50. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  51. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  52. package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  53. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
  55. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  56. package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
  57. package/packages/pi-coding-agent/src/core/tools/bash.ts +2 -2
  58. package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  59. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  60. package/packages/pi-coding-agent/src/index.ts +1 -1
  61. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  62. package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
  63. package/src/resources/extensions/bg-shell/index.ts +2 -1
  64. package/src/resources/extensions/gsd/auto.ts +18 -36
  65. package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
  66. package/src/resources/extensions/gsd/gitignore.ts +27 -0
  67. package/src/resources/extensions/gsd/guided-flow.ts +10 -9
  68. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  69. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
  70. package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
  71. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  72. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
  73. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  74. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
  75. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
  76. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  77. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  78. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  79. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  80. package/src/resources/extensions/gsd/tests/git-service.test.ts +53 -1
  81. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
  82. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
  83. 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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,IAAI,iBAAiB,GAA6C,IAAI,CAAC;AAEvE;;GAEG;AACH,SAAS,cAAc;IACtB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,oFAAoF;QACpF,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAC1C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,IAAI,UAAU,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;oBAC1C,OAAO,UAAU,CAAC;gBACnB,CAAC;YACF,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,gBAAgB;QACjB,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,kFAAkF;IAClF,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAC1C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,IAAI,UAAU,EAAE,CAAC;gBAChB,OAAO,UAAU,CAAC;YACnB,CAAC;QACF,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,gBAAgB;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC7B,IAAI,iBAAiB,EAAE,CAAC;QACvB,OAAO,iBAAiB,CAAC;IAC1B,CAAC;IAED,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAC1C,MAAM,eAAe,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC;IAEhD,qCAAqC;IACrC,IAAI,eAAe,EAAE,CAAC;QACrB,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YACjC,iBAAiB,GAAG,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7D,OAAO,iBAAiB,CAAC;QAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CACd,gCAAgC,eAAe,gCAAgC,eAAe,EAAE,EAAE,CAClG,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,qCAAqC;QACrC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAC9C,IAAI,YAAY,EAAE,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,sBAAsB,CAAC,CAAC;QACnD,CAAC;QACD,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACzD,IAAI,eAAe,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,GAAG,eAAe,sBAAsB,CAAC,CAAC;QACtD,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtB,iBAAiB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClD,OAAO,iBAAiB,CAAC;YAC1B,CAAC;QACF,CAAC;QAED,kEAAkE;QAClE,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;QACpC,IAAI,UAAU,EAAE,CAAC;YAChB,iBAAiB,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACxD,OAAO,iBAAiB,CAAC;QAC1B,CAAC;QAED,MAAM,IAAI,KAAK,CACd,iCAAiC;YAChC,kEAAkE;YAClE,oDAAoD;YACpD,yBAAyB,eAAe,EAAE,MAAM;YAChD,0BAA0B,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClE,CAAC;IACH,CAAC;IAED,8DAA8D;IAC9D,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,iBAAiB,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,OAAO,iBAAiB,CAAC;IAC1B,CAAC;IAED,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IACpC,IAAI,UAAU,EAAE,CAAC;QAChB,iBAAiB,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxD,OAAO,iBAAiB,CAAC;IAC1B,CAAC;IAED,iBAAiB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;IAClD,OAAO,iBAAiB,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,WAAW;IAC1B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC;IAC/F,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/C,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAEpG,OAAO;QACN,GAAG,OAAO,CAAC,GAAG;QACd,CAAC,OAAO,CAAC,EAAE,WAAW;KACtB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC/C,uEAAuE;IACvE,sEAAsE;IACtE,uCAAuC;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,yDAAyD;QACzD,iBAAiB;QACjB,8BAA8B;QAC9B,qDAAqD;QACrD,kCAAkC;QAClC,0CAA0C;QAE1C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAEjC,mEAAmE;QACnE,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAErC,sCAAsC;QACtC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAEjE,uEAAuE;QACvE,IAAI,IAAI,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QAE/B,uCAAuC;QACvC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;YAAE,OAAO,KAAK,CAAC;QAEnD,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC,CAAC;AACZ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IAC1C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,+CAA+C;QAC/C,IAAI,CAAC;YACJ,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE;gBACpD,KAAK,EAAE,QAAQ;gBACf,QAAQ,EAAE,IAAI;aACd,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,kCAAkC;QACnC,CAAC;IACF,CAAC;SAAM,CAAC;QACP,gCAAgC;QAChC,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACR,iEAAiE;YACjE,IAAI,CAAC;gBACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC9B,CAAC;YAAC,MAAM,CAAC;gBACR,uBAAuB;YACxB,CAAC;QACF,CAAC;IACF,CAAC;AACF,CAAC","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
- while (current.includes("/")) {
828
- current = current.substring(0, current.lastIndexOf("/"));
829
- const parent = metadata.get(current);
830
- if (parent) return parent;
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 sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
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
- taskSummaryAbsPath,
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 sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
1920
- const sliceSummaryAbsPath = join(sliceDirAbs, `${sid}-SUMMARY.md`);
1921
- const sliceUatAbsPath = join(sliceDirAbs, `${sid}-UAT.md`);
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: relSlicePath(base, mid, sid),
1915
+ slicePath: sliceRel,
1926
1916
  roadmapPath: roadmapRel,
1927
1917
  inlinedContext,
1928
- sliceSummaryAbsPath,
1929
- sliceUatAbsPath,
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 milestoneDirAbs = resolveMilestonePath(base, mid) ?? join(base, relMilestonePath(base, mid));
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
- milestoneSummaryAbsPath,
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 sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
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
- replanAbsPath,
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 assessmentRel = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
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: assessmentRel,
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 = absPath.substring(0, absPath.lastIndexOf("/"));
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.