gsd-pi 2.8.0 → 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 (143) hide show
  1. package/dist/loader.js +5 -0
  2. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  3. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  4. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  5. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  38. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  39. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  41. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  44. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  45. package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
  46. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  47. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  48. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
  49. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  50. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  51. package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
  52. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  53. package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
  54. package/package.json +6 -1
  55. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  56. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/config.js +4 -0
  58. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
  64. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  68. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  70. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  72. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  74. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
  76. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  80. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  84. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  86. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/index.js +4 -1
  88. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  91. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  93. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
  95. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  96. package/packages/pi-coding-agent/src/config.ts +5 -0
  97. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  98. package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
  99. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  100. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  101. package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
  102. package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  103. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  104. package/packages/pi-coding-agent/src/index.ts +4 -1
  105. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  106. package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
  107. package/src/resources/extensions/bg-shell/index.ts +2 -1
  108. package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
  109. package/src/resources/extensions/gsd/auto.ts +92 -49
  110. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  111. package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
  112. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  113. package/src/resources/extensions/gsd/files.ts +9 -40
  114. package/src/resources/extensions/gsd/git-service.ts +62 -17
  115. package/src/resources/extensions/gsd/gitignore.ts +28 -0
  116. package/src/resources/extensions/gsd/guided-flow.ts +49 -11
  117. package/src/resources/extensions/gsd/index.ts +111 -16
  118. package/src/resources/extensions/gsd/preferences.ts +8 -0
  119. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  120. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
  121. package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
  122. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  123. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
  124. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  125. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
  126. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
  127. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  131. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  132. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  133. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  134. package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
  135. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
  136. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
  137. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  138. package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
  139. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  140. package/src/resources/extensions/ttsr/index.ts +163 -0
  141. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  142. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  143. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -6,8 +6,9 @@ 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
+ import type { ArtifactManager } from "../artifact-manager.js";
11
12
 
12
13
  // Cached Win32 FFI handles for restoring VT input after child processes
13
14
  let _vtHandles: { GetConsoleMode: any; SetConsoleMode: any; handle: any } | null = null;
@@ -51,6 +52,7 @@ export type BashToolInput = Static<typeof bashSchema>;
51
52
  export interface BashToolDetails {
52
53
  truncation?: TruncationResult;
53
54
  fullOutputPath?: string;
55
+ artifactId?: string;
54
56
  }
55
57
 
56
58
  /**
@@ -187,12 +189,15 @@ export interface BashToolOptions {
187
189
  commandPrefix?: string;
188
190
  /** Hook to adjust command, cwd, or env before execution */
189
191
  spawnHook?: BashSpawnHook;
192
+ /** Session-scoped artifact storage. When provided, spills to artifact files instead of temp files. */
193
+ artifactManager?: ArtifactManager;
190
194
  }
191
195
 
192
196
  export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
193
197
  const ops = options?.operations ?? defaultBashOperations;
194
198
  const commandPrefix = options?.commandPrefix;
195
199
  const spawnHook = options?.spawnHook;
200
+ const artifactManager = options?.artifactManager;
196
201
 
197
202
  return {
198
203
  name: "bash",
@@ -206,13 +211,14 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
206
211
  onUpdate?,
207
212
  ) => {
208
213
  // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)
209
- const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
214
+ const resolvedCommand = sanitizeCommand(commandPrefix ? `${commandPrefix}\n${command}` : command);
210
215
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
211
216
 
212
217
  return new Promise((resolve, reject) => {
213
- // We'll stream to a temp file if output gets large
214
- let tempFilePath: string | undefined;
215
- let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
218
+ // We'll stream to a file if output gets large
219
+ let spillFilePath: string | undefined;
220
+ let spillArtifactId: string | undefined;
221
+ let spillFileStream: ReturnType<typeof createWriteStream> | undefined;
216
222
  let totalBytes = 0;
217
223
 
218
224
  // Keep a rolling buffer of the last chunk for tail truncation
@@ -224,19 +230,25 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
224
230
  const handleData = (data: Buffer) => {
225
231
  totalBytes += data.length;
226
232
 
227
- // Start writing to temp file once we exceed the threshold
228
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
229
- tempFilePath = getTempFilePath();
230
- tempFileStream = createWriteStream(tempFilePath);
233
+ // Start writing to file once we exceed the threshold
234
+ if (totalBytes > DEFAULT_MAX_BYTES && !spillFilePath) {
235
+ if (artifactManager) {
236
+ const allocated = artifactManager.allocatePath("bash");
237
+ spillFilePath = allocated.path;
238
+ spillArtifactId = allocated.id;
239
+ } else {
240
+ spillFilePath = getTempFilePath();
241
+ }
242
+ spillFileStream = createWriteStream(spillFilePath);
231
243
  // Write all buffered chunks to the file
232
244
  for (const chunk of chunks) {
233
- tempFileStream.write(chunk);
245
+ spillFileStream.write(chunk);
234
246
  }
235
247
  }
236
248
 
237
249
  // Write to temp file if we have one
238
- if (tempFileStream) {
239
- tempFileStream.write(data);
250
+ if (spillFileStream) {
251
+ spillFileStream.write(data);
240
252
  }
241
253
 
242
254
  // Keep rolling buffer of recent data
@@ -258,7 +270,7 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
258
270
  content: [{ type: "text", text: truncation.content || "" }],
259
271
  details: {
260
272
  truncation: truncation.truncated ? truncation : undefined,
261
- fullOutputPath: tempFilePath,
273
+ fullOutputPath: spillFilePath,
262
274
  },
263
275
  });
264
276
  }
@@ -272,8 +284,8 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
272
284
  })
273
285
  .then(({ exitCode }) => {
274
286
  // Close temp file stream
275
- if (tempFileStream) {
276
- tempFileStream.end();
287
+ if (spillFileStream) {
288
+ spillFileStream.end();
277
289
  }
278
290
 
279
291
  // Combine all buffered chunks
@@ -290,21 +302,22 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
290
302
  if (truncation.truncated) {
291
303
  details = {
292
304
  truncation,
293
- fullOutputPath: tempFilePath,
305
+ fullOutputPath: spillFilePath,
306
+ ...(spillArtifactId ? { artifactId: spillArtifactId } : {}),
294
307
  };
295
308
 
296
309
  // Build actionable notice
297
310
  const startLine = truncation.totalLines - truncation.outputLines + 1;
298
311
  const endLine = truncation.totalLines;
312
+ const outputRef = spillArtifactId ? `artifact://${spillArtifactId}` : spillFilePath;
299
313
 
300
314
  if (truncation.lastLinePartial) {
301
- // Edge case: last line alone > 30KB
302
315
  const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
303
- outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
316
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${outputRef}]`;
304
317
  } else if (truncation.truncatedBy === "lines") {
305
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
318
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${outputRef}]`;
306
319
  } else {
307
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
320
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${outputRef}]`;
308
321
  }
309
322
  }
310
323
 
@@ -317,8 +330,8 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
317
330
  })
318
331
  .catch((err: Error) => {
319
332
  // Close temp file stream
320
- if (tempFileStream) {
321
- tempFileStream.end();
333
+ if (spillFileStream) {
334
+ spillFileStream.end();
322
335
  }
323
336
 
324
337
  // Combine all buffered chunks for error output
@@ -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
  }
@@ -198,6 +198,9 @@ export {
198
198
  type SessionMessageEntry,
199
199
  type ThinkingLevelChangeEntry,
200
200
  } from "./core/session-manager.js";
201
+ // Blob and artifact storage
202
+ export { BlobStore, isBlobRef, parseBlobRef, externalizeImageData, resolveImageData } from "./core/blob-store.js";
203
+ export { ArtifactManager } from "./core/artifact-manager.js";
201
204
  export {
202
205
  type CompactionSettings,
203
206
  type ImageSettings,
@@ -330,4 +333,4 @@ export {
330
333
  export { copyToClipboard } from "./utils/clipboard.js";
331
334
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
332
335
  // Shell utilities
333
- 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,
@@ -181,7 +181,12 @@ export async function ensureBrowser(): Promise<{ browser: Browser; context: Brow
181
181
  // Lazy import so playwright is only loaded when actually needed
182
182
  const { chromium } = await import("playwright");
183
183
 
184
- const launchOptions: Record<string, unknown> = { headless: false };
184
+ // Auto-detect headless environments: Linux without $DISPLAY has no GUI.
185
+ // All browser tool operations (navigation, screenshots, DOM) work in headless mode.
186
+ const needsHeadless = process.platform === "linux" && !process.env.DISPLAY;
187
+ const launchOptions: Record<string, unknown> = {
188
+ headless: needsHeadless || process.env.FORCE_HEADLESS === "true",
189
+ };
185
190
  const customPath = process.env.BROWSER_PATH;
186
191
  if (customPath) launchOptions.executablePath = customPath;
187
192
  const browser = await chromium.launch(launchOptions);