wave-agent-sdk 0.18.4 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ Wave uses several environment variables to control its core functionality.
28
28
  | `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
29
29
  | `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
30
30
  | `WAVE_MAX_INPUT_TOKENS` | Maximum number of input tokens allowed. | `200000` |
31
- | `WAVE_MAX_OUTPUT_TOKENS` | Maximum number of output tokens allowed. | `16384` |
31
+ | `WAVE_MAX_OUTPUT_TOKENS` | Maximum number of output tokens allowed. | `32000` |
32
32
  | `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
33
33
  | `WAVE_AUTO_MEMORY_FREQUENCY` | Auto memory update frequency. `1` = every turn, `2` = every 2 turns, etc. | `1` |
34
34
  | `WAVE_TASK_LIST_ID` | Explicitly set the task list ID for the session. | (Session ID) |
@@ -0,0 +1,76 @@
1
+ # Wave Memory
2
+
3
+ Wave provides multiple memory layers to give the agent context-specific instructions and knowledge. Memory files are standard Markdown files loaded automatically at startup.
4
+
5
+ ## Memory Layers
6
+
7
+ Wave looks for memory in the following locations, loaded in order of increasing priority:
8
+
9
+ 1. **User Memory**: `~/.wave/AGENTS.md` — Global instructions across all projects
10
+ 2. **Project Memory**: `AGENTS.md` in the project root — Project-specific instructions
11
+ 3. **Memory Rules**: `.wave/rules/*.md` and `~/.wave/rules/*.md` — Modular, path-scoped rules (see below)
12
+
13
+ > `CLAUDE.md` is also supported as a fallback for both user and project memory, for compatibility with existing repositories.
14
+
15
+ ## User Memory
16
+
17
+ Stored at `~/.wave/AGENTS.md`. Use it for cross-project preferences and global instructions, e.g. "always write tests for new features", "prefer functional style".
18
+
19
+ ## Project Memory
20
+
21
+ ### Project Memory File
22
+
23
+ The `AGENTS.md` file in the project root is the primary project-level memory. It is checked into the repository and shared with all contributors. Use it for:
24
+
25
+ - Build and test commands (e.g. "use pnpm not npm")
26
+ - Project structure and architecture conventions
27
+ - Coding standards and patterns
28
+
29
+ ### Memory Rules
30
+
31
+ Memory rules are modular Markdown files that provide path-scoped instructions. They are discovered in:
32
+
33
+ - **Project scope**: `.wave/rules/*.md` (checked into the repo)
34
+ - **User scope**: `~/.wave/rules/*.md` (personal, not shared)
35
+
36
+ Each file can optionally include YAML frontmatter to scope rules to specific file paths:
37
+
38
+ ```markdown
39
+ ---
40
+ paths:
41
+ - "src/api/**/*.ts"
42
+ - "src/services/**/*.ts"
43
+ ---
44
+
45
+ # API and Service Guidelines
46
+
47
+ - Always use `async/await` for asynchronous operations.
48
+ - Use `Zod` for input validation.
49
+ ```
50
+
51
+ #### YAML Frontmatter Fields
52
+
53
+ - `paths`: (Optional) A list of glob patterns. The rules in this file will only be active when the agent is working with files that match these patterns. If omitted, the rules are always active.
54
+ - `priority`: (Optional) A number controlling rule precedence. Higher priority rules override lower ones on conflict.
55
+
56
+ #### How Memory Rules are Loaded
57
+
58
+ - Wave recursively discovers all `.md` files in `.wave/rules/` and `~/.wave/rules/`.
59
+ - **Path-specific activation**: If a rule has a `paths` field, it is only included in the agent's context when a file being read or modified matches the glob patterns.
60
+ - **Unconditional rules**: Rules without a `paths` field are always active.
61
+ - **Priority**: Project-level rules take priority over user-level rules if there is a conflict.
62
+
63
+ #### Best Practices
64
+
65
+ - **Keep rules focused**: Create separate files for different topics (e.g. `testing.md`, `ui-components.md`).
66
+ - **Leverage path scoping**: Use the `paths` field to keep the agent's context window clean and relevant.
67
+ - **Share with your team**: Commit `.wave/rules/` to your git repository.
68
+
69
+ ## Auto-Memory
70
+
71
+ In addition to manual memory files, Wave has an **auto-memory** feature that automatically extracts and remembers important information across sessions. This is stored in `~/.wave/projects/<project-id>/memory/MEMORY.md`.
72
+
73
+ You can control auto-memory in `settings.json`:
74
+
75
+ - `autoMemoryEnabled`: Enable or disable auto-memory (default: `true`).
76
+ - `autoMemoryFrequency`: Frequency of auto-memory extraction turns (default: `1`).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: settings
3
- description: Manage Wave settings and get guidance on settings.json, hooks, environment variables, permissions, MCP servers, memory rules, skills, subagents, plugins, and plugin marketplaces. Use this when the user wants to view, update, or learn how to configure Wave.
3
+ description: Manage Wave settings and get guidance on settings.json, hooks, environment variables, permissions, MCP servers, memory, skills, subagents, plugins, and plugin marketplaces. Use this when the user wants to view, update, or learn how to configure Wave.
4
4
  ---
5
5
 
6
6
  # Wave Settings Skill
@@ -79,9 +79,9 @@ For detailed model configuration, see [MODELS.md](${WAVE_SKILL_DIR}/MODELS.md).
79
79
  Connect to external servers to provide additional tools and context.
80
80
  For detailed MCP configuration, see [MCP.md](${WAVE_SKILL_DIR}/MCP.md).
81
81
 
82
- ### 6. Memory Rules
83
- Provide context-specific instructions and guidelines to the agent.
84
- For detailed memory rules configuration, see [MEMORY_RULES.md](${WAVE_SKILL_DIR}/MEMORY_RULES.md).
82
+ ### 6. Memory
83
+ Provide context-specific instructions and knowledge to the agent through user memory, project memory, memory rules, and auto-memory.
84
+ For detailed memory configuration, see [MEMORY.md](${WAVE_SKILL_DIR}/MEMORY.md).
85
85
 
86
86
  ### 7. Skills
87
87
  Extend Wave's functionality by creating custom skills.
@@ -1 +1 @@
1
- {"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"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"}
@@ -363,28 +363,35 @@ export class McpManager {
363
363
  cwd: this.workdir, // Use the agent's workdir as the process working directory
364
364
  stderr: "pipe", // Pipe stderr to capture it
365
365
  });
366
- // Handle stderr output for StdioClientTransport
366
+ // Buffer stderr silently (MCP servers use stderr for all logging;
367
+ // stdout is reserved for JSON-RPC). Only dump it on connection
368
+ // success (once) or failure — not line-by-line in real time.
369
+ // Aligns with Claude Code's approach to avoid noise from [INFO],
370
+ // [DEBUG], Warning: lines that are not actual errors.
367
371
  const stderr = transport.stderr;
372
+ let stderrOutput = "";
368
373
  if (stderr) {
369
- let buffer = "";
370
374
  stderr.on("data", (chunk) => {
371
- buffer += chunk.toString();
372
- const lines = buffer.split("\n");
373
- buffer = lines.pop() || "";
374
- for (const line of lines) {
375
- if (line.trim()) {
376
- logger?.error(`[MCP Server ${name}] ${line}`);
377
- }
378
- }
379
- });
380
- stderr.on("end", () => {
381
- if (buffer.trim()) {
382
- logger?.error(`[MCP Server ${name}] ${buffer}`);
375
+ if (stderrOutput.length < 64 * 1024 * 1024) {
376
+ stderrOutput += chunk.toString();
383
377
  }
384
378
  });
385
379
  }
386
380
  client = createClient();
387
- await client.connect(transport);
381
+ try {
382
+ await client.connect(transport);
383
+ }
384
+ catch (error) {
385
+ if (stderrOutput.trim()) {
386
+ logger?.error(`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`);
387
+ }
388
+ throw error;
389
+ }
390
+ // Dump accumulated stderr once after successful connection
391
+ if (stderrOutput.trim()) {
392
+ logger?.debug(`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`);
393
+ stderrOutput = "";
394
+ }
388
395
  const toolsResponse = await client.listTools();
389
396
  tools =
390
397
  toolsResponse.tools?.map((tool) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.d.ts","sourceRoot":"","sources":["../../src/tools/bashTool.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAuBtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UAmiBtB,CAAC"}
1
+ {"version":3,"file":"bashTool.d.ts","sourceRoot":"","sources":["../../src/tools/bashTool.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAuBtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UA2iBtB,CAAC"}
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import * as os from "os";
5
5
  import { logger } from "../utils/globalLogger.js";
6
6
  import { resolveShellPath } from "../utils/shellResolver.js";
7
+ import { toPosixPath } from "../utils/path.js";
7
8
  import { stripAnsiColors } from "../utils/stringUtils.js";
8
9
  import { processToolResult } from "../utils/toolResultStorage.js";
9
10
  import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
@@ -211,7 +212,8 @@ The working directory persists between commands. Try to maintain your current wo
211
212
  return new Promise((resolve) => {
212
213
  // Create a temporary file to store the CWD
213
214
  const tempCwdFile = path.join(os.tmpdir(), `wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`);
214
- const wrappedCommand = `${command} && pwd -P >| ${tempCwdFile}`;
215
+ const tempCwdFileForBash = toPosixPath(tempCwdFile);
216
+ const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
215
217
  const child = spawn(wrappedCommand, {
216
218
  shell: shellPath || true,
217
219
  stdio: "pipe",
@@ -226,6 +228,17 @@ The working directory persists between commands. Try to maintain your current wo
226
228
  let isAborted = false;
227
229
  let isBackgrounded = false;
228
230
  let isFinished = false;
231
+ // Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
232
+ const cleanupTempFile = () => {
233
+ try {
234
+ if (fs.existsSync(tempCwdFile)) {
235
+ fs.unlinkSync(tempCwdFile);
236
+ }
237
+ }
238
+ catch {
239
+ // ignore — best-effort cleanup
240
+ }
241
+ };
229
242
  const updateRealtimeResults = () => {
230
243
  if (isAborted || isBackgrounded || isFinished)
231
244
  return;
@@ -352,6 +365,7 @@ The working directory persists between commands. Try to maintain your current wo
352
365
  }
353
366
  }
354
367
  }
368
+ cleanupTempFile();
355
369
  const processedOutput = processToolResult(outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""), BASH_MAX_OUTPUT_CHARS, "bash");
356
370
  resolve({
357
371
  success: false,
@@ -410,15 +424,7 @@ The working directory persists between commands. Try to maintain your current wo
410
424
  newCwd = undefined;
411
425
  }
412
426
  finally {
413
- // Ensure temp file is cleaned up even if reading fails
414
- try {
415
- if (fs.existsSync(tempCwdFile)) {
416
- fs.unlinkSync(tempCwdFile);
417
- }
418
- }
419
- catch (fileError) {
420
- logger.error("Failed to clean up temp CWD file:", fileError);
421
- }
427
+ cleanupTempFile();
422
428
  }
423
429
  // If CWD changed, call the onCwdChange callback and add notification
424
430
  let cwdMessage;
@@ -462,6 +468,7 @@ The working directory persists between commands. Try to maintain your current wo
462
468
  if (timeoutHandle) {
463
469
  clearTimeout(timeoutHandle);
464
470
  }
471
+ cleanupTempFile();
465
472
  resolve({
466
473
  success: false,
467
474
  content: "",
@@ -1 +1 @@
1
- {"version":3,"file":"editTool.d.ts","sourceRoot":"","sources":["../../src/tools/editTool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAgBtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UAkRtB,CAAC"}
1
+ {"version":3,"file":"editTool.d.ts","sourceRoot":"","sources":["../../src/tools/editTool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AAgBtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UAgRtB,CAAC"}
@@ -95,9 +95,11 @@ Usage:
95
95
  }
96
96
  // Trigger conditional rule loading for this file
97
97
  context.messageManager?.triggerFileRead(filePath);
98
- // Enforce read-before-edit: the file must have been read first
99
- if (context.messageManager &&
100
- !context.messageManager.hasFileBeenRead(filePath)) {
98
+ // Enforce read-before-edit: the file must have been read or written first.
99
+ // readFileState is populated by Read, Write, and Edit tools — single source
100
+ // of truth, aligned with Claude Code's readFileState approach.
101
+ const resolvedPath = resolvePath(filePath, context.workdir);
102
+ if (!context.readFileState?.has(resolvedPath)) {
101
103
  return {
102
104
  success: false,
103
105
  content: "",
@@ -105,7 +107,6 @@ Usage:
105
107
  };
106
108
  }
107
109
  try {
108
- const resolvedPath = resolvePath(filePath, context.workdir);
109
110
  // Read file content
110
111
  let originalContent;
111
112
  try {
@@ -22,5 +22,5 @@ export declare const USER_MEMORY_FILE: string;
22
22
  * AI related constants
23
23
  */
24
24
  export declare const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000;
25
- export declare const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384;
25
+ export declare const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000;
26
26
  //# sourceMappingURL=constants.d.ts.map
@@ -24,4 +24,4 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
24
24
  * AI related constants
25
25
  */
26
26
  export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
27
- export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
27
+ export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000; // Default output token limit (aligned with Claude Code)
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import * as fsSync from "node:fs";
3
- import { execSync } from "node:child_process";
3
+ import { execFileSync } from "node:child_process";
4
4
  /**
5
5
  * Check if a directory is a git repository
6
6
  * @param dirPath Directory path
@@ -30,7 +30,7 @@ export function isGitRepository(dirPath) {
30
30
  */
31
31
  export function getGitRepoRoot(cwd) {
32
32
  try {
33
- return execSync("git rev-parse --show-toplevel", {
33
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
34
34
  cwd,
35
35
  encoding: "utf8",
36
36
  stdio: ["ignore", "pipe", "ignore"],
@@ -47,7 +47,7 @@ export function getGitRepoRoot(cwd) {
47
47
  */
48
48
  export function getGitCommonDir(cwd) {
49
49
  try {
50
- const commonDir = execSync("git rev-parse --git-common-dir", {
50
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
51
51
  cwd,
52
52
  encoding: "utf8",
53
53
  stdio: ["ignore", "pipe", "ignore"],
@@ -65,7 +65,7 @@ export function getGitCommonDir(cwd) {
65
65
  */
66
66
  export function getGitMainRepoRoot(cwd) {
67
67
  try {
68
- const output = execSync("git worktree list --porcelain", {
68
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
69
69
  cwd,
70
70
  encoding: "utf8",
71
71
  stdio: ["ignore", "pipe", "ignore"],
@@ -223,7 +223,7 @@ export function getDefaultRemoteBranch(cwd) {
223
223
  */
224
224
  export function hasUncommittedChanges(cwd) {
225
225
  try {
226
- const status = execSync("git status --porcelain", {
226
+ const status = execFileSync("git", ["status", "--porcelain"], {
227
227
  cwd,
228
228
  encoding: "utf8",
229
229
  stdio: ["ignore", "pipe", "ignore"],
@@ -243,7 +243,7 @@ export function hasUncommittedChanges(cwd) {
243
243
  export function hasNewCommits(cwd, baseBranch) {
244
244
  try {
245
245
  const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
246
- const log = execSync(`git log ${range} --oneline`, {
246
+ const log = execFileSync("git", ["log", range, "--oneline"], {
247
247
  cwd,
248
248
  encoding: "utf8",
249
249
  stdio: ["ignore", "pipe", "ignore"],
@@ -1 +1 @@
1
- {"version":3,"file":"openaiClient.d.ts","sourceRoot":"","sources":["../../src/utils/openaiClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sCAAsC,EACtC,mCAAmC,EACnC,mBAAmB,EACnB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyBnD,KAAK,YAAY,GACb,sCAAsC,GACtC,mCAAmC,CAAC;AAExC,UAAU,WAAW,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,UAAU,UAAU,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACxC,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CACzC;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEzC,IAAI,IAAI;;qBAGO,CAAC,SAAS,YAAY,UACrB,CAAC,YACC;gBAAE,MAAM,CAAC,EAAE,WAAW,CAAA;aAAE,KACjC,UAAU,CACX,CAAC,SAAS,mCAAmC,GACzC,aAAa,CAAC,mBAAmB,CAAC,GAClC,cAAc,CACnB;;MA2BN;YAEa,OAAO;YA0IN,oBAAoB;CAyCpC"}
1
+ {"version":3,"file":"openaiClient.d.ts","sourceRoot":"","sources":["../../src/utils/openaiClient.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sCAAsC,EACtC,mCAAmC,EACnC,mBAAmB,EACnB,cAAc,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyBnD,KAAK,YAAY,GACb,sCAAsC,GACtC,mCAAmC,CAAC;AAExC,UAAU,WAAW,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,UAAU,UAAU,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,CAAC,CAAC;IACxC,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CACzC;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEzC,IAAI,IAAI;;qBAGO,CAAC,SAAS,YAAY,UACrB,CAAC,YACC;gBAAE,MAAM,CAAC,EAAE,WAAW,CAAA;aAAE,KACjC,UAAU,CACX,CAAC,SAAS,mCAAmC,GACzC,aAAa,CAAC,mBAAmB,CAAC,GAClC,cAAc,CACnB;;MA2BN;YAEa,OAAO;YAuHN,oBAAoB;CAyCpC"}
@@ -91,36 +91,16 @@ export class OpenAIClient {
91
91
  };
92
92
  }
93
93
  }
94
- let errorBody;
94
+ let responseText = "";
95
95
  try {
96
- const text = await response.text();
97
- try {
98
- errorBody = JSON.parse(text);
99
- }
100
- catch (e) {
101
- logger.error("Failed to parse error response body as JSON", {
102
- error: e,
103
- text,
104
- });
105
- errorBody = text;
106
- }
96
+ responseText = await response.text();
107
97
  }
108
98
  catch (e) {
109
99
  logger.error("Failed to read error response text", { error: e });
110
- errorBody = {};
111
100
  }
112
- const error = new Error(typeof errorBody === "object" &&
113
- errorBody !== null &&
114
- "error" in errorBody &&
115
- typeof errorBody.error === "object" &&
116
- errorBody.error !== null &&
117
- "message" in errorBody.error
118
- ? String(errorBody.error.message)
119
- : typeof errorBody === "string"
120
- ? errorBody
121
- : response.statusText);
101
+ const error = new Error(`HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`);
122
102
  error.status = response.status;
123
- error.body = errorBody;
103
+ error.body = responseText;
124
104
  const retryableStatus = response.status === 429 ||
125
105
  (response.status >= 500 && response.status !== 501);
126
106
  if (retryableStatus && attempt < MAX_RETRIES) {
@@ -136,7 +116,7 @@ export class OpenAIClient {
136
116
  logger.error("OpenAI API Error:", {
137
117
  status: response.status,
138
118
  statusText: response.statusText,
139
- errorBody,
119
+ body: responseText,
140
120
  });
141
121
  throw error;
142
122
  }
@@ -12,4 +12,11 @@ export declare function resolvePath(filePath: string, workdir: string): string;
12
12
  * @returns Path for display (relative or absolute)
13
13
  */
14
14
  export declare function getDisplayPath(filePath: string, workdir: string): string;
15
+ /**
16
+ * Convert backslashes to forward slashes for shell compatibility on Windows.
17
+ * On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
18
+ * which are treated as escape characters when interpolated into shell command strings.
19
+ * Returns the path as-is on non-Windows platforms.
20
+ */
21
+ export declare function toPosixPath(p: string): string;
15
22
  //# sourceMappingURL=path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/utils/path.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAarE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAwBxE"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/utils/path.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAarE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAwBxE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -46,3 +46,12 @@ export function getDisplayPath(filePath, workdir) {
46
46
  }
47
47
  return filePath;
48
48
  }
49
+ /**
50
+ * Convert backslashes to forward slashes for shell compatibility on Windows.
51
+ * On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
52
+ * which are treated as escape characters when interpolated into shell command strings.
53
+ * Returns the path as-is on non-Windows platforms.
54
+ */
55
+ export function toPosixPath(p) {
56
+ return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
57
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"worktreeUtils.d.ts","sourceRoot":"","sources":["../../src/utils/worktreeUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBvD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA6B7C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAuJtE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA+DvD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA8BlD"}
1
+ {"version":3,"file":"worktreeUtils.d.ts","sourceRoot":"","sources":["../../src/utils/worktreeUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmBvD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA6B7C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CA6JtE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmEvD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACrC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAsClD"}
@@ -2,7 +2,7 @@
2
2
  * Git worktree creation and removal utilities for the SDK.
3
3
  * Used by EnterWorktree and ExitWorktree tools.
4
4
  */
5
- import { execSync } from "node:child_process";
5
+ import { execFileSync } from "node:child_process";
6
6
  import * as path from "node:path";
7
7
  import * as fs from "node:fs";
8
8
  import { getGitMainRepoRoot, getDefaultRemoteBranch } from "./gitUtils.js";
@@ -61,7 +61,7 @@ export function generateWorktreeName() {
61
61
  * Get the current HEAD commit SHA.
62
62
  */
63
63
  export function getHeadCommit(cwd) {
64
- return execSync(`git -C "${cwd}" rev-parse HEAD`, {
64
+ return execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], {
65
65
  encoding: "utf8",
66
66
  stdio: ["ignore", "pipe", "ignore"],
67
67
  }).trim();
@@ -97,7 +97,7 @@ export function createWorktree(name, cwd) {
97
97
  }
98
98
  try {
99
99
  // Create worktree and branch
100
- execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
100
+ execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
101
101
  cwd: repoRoot,
102
102
  stdio: ["ignore", "pipe", "pipe"],
103
103
  env: {
@@ -120,7 +120,7 @@ export function createWorktree(name, cwd) {
120
120
  if (stderr.includes("already exists")) {
121
121
  // Branch exists but worktree doesn't — attach to existing branch
122
122
  try {
123
- execSync(`git worktree add "${worktreePath}" ${branchName}`, {
123
+ execFileSync("git", ["worktree", "add", worktreePath, branchName], {
124
124
  cwd: repoRoot,
125
125
  stdio: ["ignore", "pipe", "pipe"],
126
126
  env: {
@@ -147,7 +147,7 @@ export function createWorktree(name, cwd) {
147
147
  // Base branch not fetched yet — try fetching then retrying
148
148
  const branchNameOnly = baseBranch.split("/").pop();
149
149
  try {
150
- execSync(`git fetch origin ${branchNameOnly}`, {
150
+ execFileSync("git", ["fetch", "origin", branchNameOnly], {
151
151
  cwd: repoRoot,
152
152
  stdio: ["ignore", "pipe", "pipe"],
153
153
  env: {
@@ -156,7 +156,7 @@ export function createWorktree(name, cwd) {
156
156
  GIT_ASKPASS: "",
157
157
  },
158
158
  });
159
- execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
159
+ execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
160
160
  cwd: repoRoot,
161
161
  stdio: ["ignore", "pipe", "pipe"],
162
162
  env: {
@@ -177,7 +177,7 @@ export function createWorktree(name, cwd) {
177
177
  catch {
178
178
  // Fetch or retry failed — fall back to HEAD
179
179
  try {
180
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
180
+ execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
181
181
  cwd: repoRoot,
182
182
  stdio: ["ignore", "pipe", "pipe"],
183
183
  env: {
@@ -212,7 +212,7 @@ export function removeWorktree(info) {
212
212
  // Get current branch in worktree before removing
213
213
  let currentBranch;
214
214
  try {
215
- currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
215
+ currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
216
216
  cwd: info.path,
217
217
  encoding: "utf8",
218
218
  stdio: ["ignore", "pipe", "ignore"],
@@ -222,13 +222,13 @@ export function removeWorktree(info) {
222
222
  // Ignore errors
223
223
  }
224
224
  // Remove worktree
225
- execSync(`git worktree remove --force "${info.path}"`, {
225
+ execFileSync("git", ["worktree", "remove", "--force", info.path], {
226
226
  cwd: repoRoot,
227
227
  stdio: ["ignore", "pipe", "pipe"],
228
228
  });
229
229
  // Delete worktree branch
230
230
  try {
231
- execSync(`git branch -D ${info.branch}`, {
231
+ execFileSync("git", ["branch", "-D", info.branch], {
232
232
  cwd: repoRoot,
233
233
  stdio: ["ignore", "pipe", "pipe"],
234
234
  });
@@ -246,7 +246,7 @@ export function removeWorktree(info) {
246
246
  currentBranch !== "main" &&
247
247
  currentBranch !== "master") {
248
248
  try {
249
- execSync(`git branch -D ${currentBranch}`, {
249
+ execFileSync("git", ["branch", "-D", currentBranch], {
250
250
  cwd: repoRoot,
251
251
  stdio: ["ignore", "pipe", "pipe"],
252
252
  });
@@ -271,7 +271,7 @@ export function removeWorktree(info) {
271
271
  */
272
272
  export function countWorktreeChanges(worktreePath, originalHeadCommit) {
273
273
  try {
274
- const statusOutput = execSync(`git -C "${worktreePath}" status --porcelain`, {
274
+ const statusOutput = execFileSync("git", ["-C", worktreePath, "status", "--porcelain"], {
275
275
  encoding: "utf8",
276
276
  stdio: ["ignore", "pipe", "ignore"],
277
277
  });
@@ -281,7 +281,13 @@ export function countWorktreeChanges(worktreePath, originalHeadCommit) {
281
281
  if (!originalHeadCommit) {
282
282
  return null;
283
283
  }
284
- const revListOutput = execSync(`git -C "${worktreePath}" rev-list --count ${originalHeadCommit}..HEAD`, {
284
+ const revListOutput = execFileSync("git", [
285
+ "-C",
286
+ worktreePath,
287
+ "rev-list",
288
+ "--count",
289
+ `${originalHeadCommit}..HEAD`,
290
+ ], {
285
291
  encoding: "utf8",
286
292
  stdio: ["ignore", "pipe", "ignore"],
287
293
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -484,29 +484,40 @@ export class McpManager {
484
484
  stderr: "pipe", // Pipe stderr to capture it
485
485
  });
486
486
 
487
- // Handle stderr output for StdioClientTransport
487
+ // Buffer stderr silently (MCP servers use stderr for all logging;
488
+ // stdout is reserved for JSON-RPC). Only dump it on connection
489
+ // success (once) or failure — not line-by-line in real time.
490
+ // Aligns with Claude Code's approach to avoid noise from [INFO],
491
+ // [DEBUG], Warning: lines that are not actual errors.
488
492
  const stderr = (transport as StdioClientTransport).stderr;
493
+ let stderrOutput = "";
489
494
  if (stderr) {
490
- let buffer = "";
491
495
  stderr.on("data", (chunk: Buffer) => {
492
- buffer += chunk.toString();
493
- const lines = buffer.split("\n");
494
- buffer = lines.pop() || "";
495
- for (const line of lines) {
496
- if (line.trim()) {
497
- logger?.error(`[MCP Server ${name}] ${line}`);
498
- }
499
- }
500
- });
501
- stderr.on("end", () => {
502
- if (buffer.trim()) {
503
- logger?.error(`[MCP Server ${name}] ${buffer}`);
496
+ if (stderrOutput.length < 64 * 1024 * 1024) {
497
+ stderrOutput += chunk.toString();
504
498
  }
505
499
  });
506
500
  }
507
501
 
508
502
  client = createClient();
509
- await client.connect(transport);
503
+ try {
504
+ await client.connect(transport);
505
+ } catch (error) {
506
+ if (stderrOutput.trim()) {
507
+ logger?.error(
508
+ `[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
509
+ );
510
+ }
511
+ throw error;
512
+ }
513
+
514
+ // Dump accumulated stderr once after successful connection
515
+ if (stderrOutput.trim()) {
516
+ logger?.debug(
517
+ `[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
518
+ );
519
+ stderrOutput = "";
520
+ }
510
521
 
511
522
  const toolsResponse = await client.listTools();
512
523
  tools =
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import * as os from "os";
5
5
  import { logger } from "../utils/globalLogger.js";
6
6
  import { resolveShellPath } from "../utils/shellResolver.js";
7
+ import { toPosixPath } from "../utils/path.js";
7
8
  import { stripAnsiColors } from "../utils/stringUtils.js";
8
9
  import { processToolResult } from "../utils/toolResultStorage.js";
9
10
  import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
@@ -246,7 +247,8 @@ The working directory persists between commands. Try to maintain your current wo
246
247
  os.tmpdir(),
247
248
  `wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
248
249
  );
249
- const wrappedCommand = `${command} && pwd -P >| ${tempCwdFile}`;
250
+ const tempCwdFileForBash = toPosixPath(tempCwdFile);
251
+ const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
250
252
 
251
253
  const child: ChildProcess = spawn(wrappedCommand, {
252
254
  shell: shellPath || true,
@@ -264,6 +266,17 @@ The working directory persists between commands. Try to maintain your current wo
264
266
  let isBackgrounded = false;
265
267
  let isFinished = false;
266
268
 
269
+ // Best-effort cleanup of the temp CWD file — used by abort/error/exit paths
270
+ const cleanupTempFile = () => {
271
+ try {
272
+ if (fs.existsSync(tempCwdFile)) {
273
+ fs.unlinkSync(tempCwdFile);
274
+ }
275
+ } catch {
276
+ // ignore — best-effort cleanup
277
+ }
278
+ };
279
+
267
280
  const updateRealtimeResults = () => {
268
281
  if (isAborted || isBackgrounded || isFinished) return;
269
282
 
@@ -420,6 +433,8 @@ The working directory persists between commands. Try to maintain your current wo
420
433
  }
421
434
  }
422
435
 
436
+ cleanupTempFile();
437
+
423
438
  const processedOutput = processToolResult(
424
439
  outputBuffer + (errorBuffer ? "\n" + errorBuffer : ""),
425
440
  BASH_MAX_OUTPUT_CHARS,
@@ -491,14 +506,7 @@ The working directory persists between commands. Try to maintain your current wo
491
506
  );
492
507
  newCwd = undefined;
493
508
  } finally {
494
- // Ensure temp file is cleaned up even if reading fails
495
- try {
496
- if (fs.existsSync(tempCwdFile)) {
497
- fs.unlinkSync(tempCwdFile);
498
- }
499
- } catch (fileError) {
500
- logger.error("Failed to clean up temp CWD file:", fileError);
501
- }
509
+ cleanupTempFile();
502
510
  }
503
511
 
504
512
  // If CWD changed, call the onCwdChange callback and add notification
@@ -560,6 +568,7 @@ The working directory persists between commands. Try to maintain your current wo
560
568
  if (timeoutHandle) {
561
569
  clearTimeout(timeoutHandle);
562
570
  }
571
+ cleanupTempFile();
563
572
  resolve({
564
573
  success: false,
565
574
  content: "",
@@ -112,11 +112,11 @@ Usage:
112
112
  // Trigger conditional rule loading for this file
113
113
  context.messageManager?.triggerFileRead(filePath);
114
114
 
115
- // Enforce read-before-edit: the file must have been read first
116
- if (
117
- context.messageManager &&
118
- !context.messageManager.hasFileBeenRead(filePath)
119
- ) {
115
+ // Enforce read-before-edit: the file must have been read or written first.
116
+ // readFileState is populated by Read, Write, and Edit tools — single source
117
+ // of truth, aligned with Claude Code's readFileState approach.
118
+ const resolvedPath = resolvePath(filePath, context.workdir);
119
+ if (!context.readFileState?.has(resolvedPath)) {
120
120
  return {
121
121
  success: false,
122
122
  content: "",
@@ -125,8 +125,6 @@ Usage:
125
125
  }
126
126
 
127
127
  try {
128
- const resolvedPath = resolvePath(filePath, context.workdir);
129
-
130
128
  // Read file content
131
129
  let originalContent: string;
132
130
  try {
@@ -30,4 +30,4 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
30
30
  * AI related constants
31
31
  */
32
32
  export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 200000; // Default token limit
33
- export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
33
+ export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 32000; // Default output token limit (aligned with Claude Code)
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import * as fsSync from "node:fs";
3
- import { execSync } from "node:child_process";
3
+ import { execFileSync } from "node:child_process";
4
4
 
5
5
  /**
6
6
  * Check if a directory is a git repository
@@ -31,7 +31,7 @@ export function isGitRepository(dirPath: string): string {
31
31
  */
32
32
  export function getGitRepoRoot(cwd: string): string {
33
33
  try {
34
- return execSync("git rev-parse --show-toplevel", {
34
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
35
35
  cwd,
36
36
  encoding: "utf8",
37
37
  stdio: ["ignore", "pipe", "ignore"],
@@ -48,7 +48,7 @@ export function getGitRepoRoot(cwd: string): string {
48
48
  */
49
49
  export function getGitCommonDir(cwd: string): string {
50
50
  try {
51
- const commonDir = execSync("git rev-parse --git-common-dir", {
51
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
52
52
  cwd,
53
53
  encoding: "utf8",
54
54
  stdio: ["ignore", "pipe", "ignore"],
@@ -66,7 +66,7 @@ export function getGitCommonDir(cwd: string): string {
66
66
  */
67
67
  export function getGitMainRepoRoot(cwd: string): string {
68
68
  try {
69
- const output = execSync("git worktree list --porcelain", {
69
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
70
70
  cwd,
71
71
  encoding: "utf8",
72
72
  stdio: ["ignore", "pipe", "ignore"],
@@ -229,7 +229,7 @@ export function getDefaultRemoteBranch(cwd: string): string {
229
229
  */
230
230
  export function hasUncommittedChanges(cwd: string): boolean {
231
231
  try {
232
- const status = execSync("git status --porcelain", {
232
+ const status = execFileSync("git", ["status", "--porcelain"], {
233
233
  cwd,
234
234
  encoding: "utf8",
235
235
  stdio: ["ignore", "pipe", "ignore"],
@@ -249,7 +249,7 @@ export function hasUncommittedChanges(cwd: string): boolean {
249
249
  export function hasNewCommits(cwd: string, baseBranch?: string): boolean {
250
250
  try {
251
251
  const range = baseBranch ? `${baseBranch}..HEAD` : "@{u}..HEAD";
252
- const log = execSync(`git log ${range} --oneline`, {
252
+ const log = execFileSync("git", ["log", range, "--oneline"], {
253
253
  cwd,
254
254
  encoding: "utf8",
255
255
  stdio: ["ignore", "pipe", "ignore"],
@@ -166,37 +166,18 @@ export class OpenAIClient {
166
166
  }
167
167
  }
168
168
 
169
- let errorBody: unknown;
169
+ let responseText = "";
170
170
  try {
171
- const text = await response.text();
172
- try {
173
- errorBody = JSON.parse(text);
174
- } catch (e) {
175
- logger.error("Failed to parse error response body as JSON", {
176
- error: e,
177
- text,
178
- });
179
- errorBody = text;
180
- }
171
+ responseText = await response.text();
181
172
  } catch (e) {
182
173
  logger.error("Failed to read error response text", { error: e });
183
- errorBody = {};
184
174
  }
185
175
 
186
176
  const error = new Error(
187
- typeof errorBody === "object" &&
188
- errorBody !== null &&
189
- "error" in errorBody &&
190
- typeof (errorBody as { error: unknown }).error === "object" &&
191
- (errorBody as { error: object }).error !== null &&
192
- "message" in (errorBody as { error: { message: unknown } }).error
193
- ? String((errorBody as { error: { message: string } }).error.message)
194
- : typeof errorBody === "string"
195
- ? errorBody
196
- : response.statusText,
177
+ `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`,
197
178
  ) as Error & { status?: number; body?: unknown };
198
179
  error.status = response.status;
199
- error.body = errorBody;
180
+ error.body = responseText;
200
181
 
201
182
  const retryableStatus =
202
183
  response.status === 429 ||
@@ -215,7 +196,7 @@ export class OpenAIClient {
215
196
  logger.error("OpenAI API Error:", {
216
197
  status: response.status,
217
198
  statusText: response.statusText,
218
- errorBody,
199
+ body: responseText,
219
200
  });
220
201
  throw error;
221
202
  }
package/src/utils/path.ts CHANGED
@@ -54,3 +54,13 @@ export function getDisplayPath(filePath: string, workdir: string): string {
54
54
  }
55
55
  return filePath;
56
56
  }
57
+
58
+ /**
59
+ * Convert backslashes to forward slashes for shell compatibility on Windows.
60
+ * On Windows, os.tmpdir() returns paths with backslashes (e.g., C:\Users\foo\Temp),
61
+ * which are treated as escape characters when interpolated into shell command strings.
62
+ * Returns the path as-is on non-Windows platforms.
63
+ */
64
+ export function toPosixPath(p: string): string {
65
+ return process.platform === "win32" ? p.replace(/\\/g, "/") : p;
66
+ }
@@ -3,7 +3,7 @@
3
3
  * Used by EnterWorktree and ExitWorktree tools.
4
4
  */
5
5
 
6
- import { execSync } from "node:child_process";
6
+ import { execFileSync } from "node:child_process";
7
7
  import * as path from "node:path";
8
8
  import * as fs from "node:fs";
9
9
  import { getGitMainRepoRoot, getDefaultRemoteBranch } from "./gitUtils.js";
@@ -81,7 +81,7 @@ export function generateWorktreeName(): string {
81
81
  * Get the current HEAD commit SHA.
82
82
  */
83
83
  export function getHeadCommit(cwd: string): string {
84
- return execSync(`git -C "${cwd}" rev-parse HEAD`, {
84
+ return execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], {
85
85
  encoding: "utf8",
86
86
  stdio: ["ignore", "pipe", "ignore"],
87
87
  }).trim();
@@ -125,8 +125,9 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
125
125
 
126
126
  try {
127
127
  // Create worktree and branch
128
- execSync(
129
- `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`,
128
+ execFileSync(
129
+ "git",
130
+ ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
130
131
  {
131
132
  cwd: repoRoot,
132
133
  stdio: ["ignore", "pipe", "pipe"],
@@ -151,7 +152,7 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
151
152
  if (stderr.includes("already exists")) {
152
153
  // Branch exists but worktree doesn't — attach to existing branch
153
154
  try {
154
- execSync(`git worktree add "${worktreePath}" ${branchName}`, {
155
+ execFileSync("git", ["worktree", "add", worktreePath, branchName], {
155
156
  cwd: repoRoot,
156
157
  stdio: ["ignore", "pipe", "pipe"],
157
158
  env: {
@@ -181,7 +182,7 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
181
182
  // Base branch not fetched yet — try fetching then retrying
182
183
  const branchNameOnly = baseBranch.split("/").pop()!;
183
184
  try {
184
- execSync(`git fetch origin ${branchNameOnly}`, {
185
+ execFileSync("git", ["fetch", "origin", branchNameOnly], {
185
186
  cwd: repoRoot,
186
187
  stdio: ["ignore", "pipe", "pipe"],
187
188
  env: {
@@ -190,8 +191,9 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
190
191
  GIT_ASKPASS: "",
191
192
  },
192
193
  });
193
- execSync(
194
- `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`,
194
+ execFileSync(
195
+ "git",
196
+ ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
195
197
  {
196
198
  cwd: repoRoot,
197
199
  stdio: ["ignore", "pipe", "pipe"],
@@ -213,15 +215,19 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
213
215
  } catch {
214
216
  // Fetch or retry failed — fall back to HEAD
215
217
  try {
216
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
217
- cwd: repoRoot,
218
- stdio: ["ignore", "pipe", "pipe"],
219
- env: {
220
- ...process.env,
221
- GIT_TERMINAL_PROMPT: "0",
222
- GIT_ASKPASS: "",
218
+ execFileSync(
219
+ "git",
220
+ ["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
221
+ {
222
+ cwd: repoRoot,
223
+ stdio: ["ignore", "pipe", "pipe"],
224
+ env: {
225
+ ...process.env,
226
+ GIT_TERMINAL_PROMPT: "0",
227
+ GIT_ASKPASS: "",
228
+ },
223
229
  },
224
- });
230
+ );
225
231
  return {
226
232
  name,
227
233
  path: worktreePath,
@@ -253,24 +259,28 @@ export function removeWorktree(info: WorktreeInfo): void {
253
259
  // Get current branch in worktree before removing
254
260
  let currentBranch: string | undefined;
255
261
  try {
256
- currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
257
- cwd: info.path,
258
- encoding: "utf8",
259
- stdio: ["ignore", "pipe", "ignore"],
260
- }).trim();
262
+ currentBranch = execFileSync(
263
+ "git",
264
+ ["rev-parse", "--abbrev-ref", "HEAD"],
265
+ {
266
+ cwd: info.path,
267
+ encoding: "utf8",
268
+ stdio: ["ignore", "pipe", "ignore"],
269
+ },
270
+ ).trim();
261
271
  } catch {
262
272
  // Ignore errors
263
273
  }
264
274
 
265
275
  // Remove worktree
266
- execSync(`git worktree remove --force "${info.path}"`, {
276
+ execFileSync("git", ["worktree", "remove", "--force", info.path], {
267
277
  cwd: repoRoot,
268
278
  stdio: ["ignore", "pipe", "pipe"],
269
279
  });
270
280
 
271
281
  // Delete worktree branch
272
282
  try {
273
- execSync(`git branch -D ${info.branch}`, {
283
+ execFileSync("git", ["branch", "-D", info.branch], {
274
284
  cwd: repoRoot,
275
285
  stdio: ["ignore", "pipe", "pipe"],
276
286
  });
@@ -293,7 +303,7 @@ export function removeWorktree(info: WorktreeInfo): void {
293
303
  currentBranch !== "master"
294
304
  ) {
295
305
  try {
296
- execSync(`git branch -D ${currentBranch}`, {
306
+ execFileSync("git", ["branch", "-D", currentBranch], {
297
307
  cwd: repoRoot,
298
308
  stdio: ["ignore", "pipe", "pipe"],
299
309
  });
@@ -320,8 +330,9 @@ export function countWorktreeChanges(
320
330
  originalHeadCommit: string | undefined,
321
331
  ): { changedFiles: number; commits: number } | null {
322
332
  try {
323
- const statusOutput = execSync(
324
- `git -C "${worktreePath}" status --porcelain`,
333
+ const statusOutput = execFileSync(
334
+ "git",
335
+ ["-C", worktreePath, "status", "--porcelain"],
325
336
  {
326
337
  encoding: "utf8",
327
338
  stdio: ["ignore", "pipe", "ignore"],
@@ -335,8 +346,15 @@ export function countWorktreeChanges(
335
346
  return null;
336
347
  }
337
348
 
338
- const revListOutput = execSync(
339
- `git -C "${worktreePath}" rev-list --count ${originalHeadCommit}..HEAD`,
349
+ const revListOutput = execFileSync(
350
+ "git",
351
+ [
352
+ "-C",
353
+ worktreePath,
354
+ "rev-list",
355
+ "--count",
356
+ `${originalHeadCommit}..HEAD`,
357
+ ],
340
358
  {
341
359
  encoding: "utf8",
342
360
  stdio: ["ignore", "pipe", "ignore"],
@@ -1,60 +0,0 @@
1
- # Wave Memory Rules Configuration
2
-
3
- Memory rules allow you to provide context-specific instructions and guidelines to the agent. This document explains how to create and manage memory rules in Wave.
4
-
5
- ## What are Memory Rules?
6
-
7
- Memory rules are Markdown files that contain instructions for the agent. They are used to:
8
- - Enforce coding styles and conventions.
9
- - Provide project-specific context (e.g., "always use pnpm").
10
- - Define architectural patterns and best practices.
11
- - Share common knowledge across the team.
12
-
13
- ## Creating Memory Rules
14
-
15
- Wave looks for memory rules in the following locations:
16
-
17
- 1. **User Scope**: `~/.wave/rules/*.md` (Global memory rules)
18
- 2. **Project Scope**: `.wave/rules/*.md` (Project-specific memory rules)
19
- 3. **Project Root**: `AGENTS.md` (Legacy project-level memory rules)
20
-
21
- ### File Structure
22
-
23
- A memory rule file is a standard Markdown file. It can optionally include YAML frontmatter to scope the rules to specific file paths.
24
-
25
- ```markdown
26
- ---
27
- paths:
28
- - "src/api/**/*.ts"
29
- - "src/services/**/*.ts"
30
- ---
31
-
32
- # API and Service Guidelines
33
-
34
- - Always use `async/await` for asynchronous operations.
35
- - Use `Zod` for input validation.
36
- - Follow the repository pattern for data access.
37
- ```
38
-
39
- ### YAML Frontmatter Fields
40
-
41
- - `paths`: (Optional) A list of glob patterns. The rules in this file will only be active when the agent is working with files that match these patterns. If omitted, the rules are always active.
42
-
43
- ## How Memory Rules are Loaded
44
-
45
- Wave automatically discovers and loads all `.md` files in the `.wave/rules/` directory and its immediate subdirectories.
46
-
47
- - **Path-Specific Activation**: If a memory rule has a `paths` field, it is only included in the agent's context if *any* file currently being read or modified matches the glob patterns.
48
- - **Union of Rules**: If multiple files are in context, Wave activates the union of all matching memory rules.
49
- - **Priority**: Project-level memory rules take priority over user-level memory rules if there is a conflict.
50
-
51
- ## Best Practices
52
-
53
- - **Keep rules focused**: Create separate files for different topics (e.g., `testing.md`, `ui-components.md`).
54
- - **Use clear instructions**: Write rules in a way that is easy for the agent to understand and follow.
55
- - **Leverage path scoping**: Use the `paths` field to keep the agent's context window clean and relevant.
56
- - **Share rules with your team**: Commit `.wave/rules/` to your git repository to ensure everyone on the team has the same context.
57
-
58
- ## Auto-Memory
59
-
60
- In addition to manual memory rules, Wave also has an **auto-memory** feature that automatically remembers important information across sessions. This is stored in `~/.wave/projects/<project-id>/memory/MEMORY.md`. You can disable this feature in `settings.json` by setting `"autoMemoryEnabled": false`.