wave-agent-sdk 0.18.4 → 0.18.6

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 (37) hide show
  1. package/builtin/skills/settings/ENV.md +1 -1
  2. package/builtin/skills/settings/MEMORY.md +76 -0
  3. package/builtin/skills/settings/SKILL.md +4 -4
  4. package/dist/managers/aiManager.js +80 -82
  5. package/dist/managers/mcpManager.d.ts.map +1 -1
  6. package/dist/managers/mcpManager.js +22 -15
  7. package/dist/tools/bashTool.d.ts.map +1 -1
  8. package/dist/tools/bashTool.js +17 -10
  9. package/dist/tools/editTool.d.ts.map +1 -1
  10. package/dist/tools/editTool.js +5 -4
  11. package/dist/utils/constants.d.ts +1 -1
  12. package/dist/utils/constants.js +1 -1
  13. package/dist/utils/gitUtils.js +6 -6
  14. package/dist/utils/openaiClient.d.ts.map +1 -1
  15. package/dist/utils/openaiClient.js +7 -25
  16. package/dist/utils/path.d.ts +7 -0
  17. package/dist/utils/path.d.ts.map +1 -1
  18. package/dist/utils/path.js +9 -0
  19. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  20. package/dist/utils/worktreeUtils.js +19 -13
  21. package/package.json +1 -1
  22. package/scripts/install_ripgrep.js +1 -21
  23. package/src/managers/aiManager.ts +97 -97
  24. package/src/managers/mcpManager.ts +26 -15
  25. package/src/tools/bashTool.ts +18 -9
  26. package/src/tools/editTool.ts +5 -7
  27. package/src/utils/constants.ts +1 -1
  28. package/src/utils/gitUtils.ts +6 -6
  29. package/src/utils/openaiClient.ts +7 -24
  30. package/src/utils/path.ts +10 -0
  31. package/src/utils/worktreeUtils.ts +46 -28
  32. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  33. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  34. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  35. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  36. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
  37. package/builtin/skills/settings/MEMORY_RULES.md +0 -60
@@ -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.6",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -9,18 +9,6 @@ const __dirname = path.dirname(__filename);
9
9
  const MANIFEST_PATH = path.resolve(__dirname, "../bin/rg");
10
10
  const VENDOR_DIR = path.resolve(__dirname, "../vendor/ripgrep");
11
11
 
12
- function getCurrentPlatform() {
13
- const platform = process.platform;
14
- const arch = process.arch;
15
- if (platform === "darwin")
16
- return `macos-${arch === "arm64" ? "aarch64" : "x86_64"}`;
17
- if (platform === "linux")
18
- return `linux-${arch === "arm64" ? "aarch64" : "x86_64"}`;
19
- if (platform === "win32")
20
- return `windows-${arch === "arm64" ? "aarch64" : "x86_64"}`;
21
- return null;
22
- }
23
-
24
12
  async function main() {
25
13
  if (!fs.existsSync(MANIFEST_PATH)) {
26
14
  console.error(`Manifest not found: ${MANIFEST_PATH}`);
@@ -30,15 +18,7 @@ async function main() {
30
18
  const manifestContent = fs.readFileSync(MANIFEST_PATH, "utf-8");
31
19
  const jsonContent = manifestContent.replace(/^#!.*\n/, "");
32
20
  const manifest = JSON.parse(jsonContent);
33
- const allPlatforms = manifest.platforms;
34
-
35
- // In CI, only download for the current platform
36
- const isCI = process.env.CI === "true";
37
- const currentPlatform = isCI ? getCurrentPlatform() : null;
38
- const platforms =
39
- currentPlatform && currentPlatform in allPlatforms
40
- ? { [currentPlatform]: allPlatforms[currentPlatform] }
41
- : allPlatforms;
21
+ const platforms = manifest.platforms;
42
22
 
43
23
  if (!fs.existsSync(VENDOR_DIR)) {
44
24
  fs.mkdirSync(VENDOR_DIR, { recursive: true });
@@ -1271,7 +1271,103 @@ export class AIManager {
1271
1271
  // Set loading to false first
1272
1272
  this.setIsLoading(false);
1273
1273
 
1274
- // Inject pending notifications from background tasks
1274
+ // Clear temporary rules
1275
+ this.permissionManager?.clearTemporaryRules();
1276
+
1277
+ // Clear abort controllers
1278
+ this.abortController = null;
1279
+ this.toolAbortController = null;
1280
+
1281
+ // Execute Stop/SubagentStop hooks only if the operation was not aborted
1282
+ const isCurrentlyAborted =
1283
+ abortController.signal.aborted || toolAbortController.signal.aborted;
1284
+
1285
+ if (!isCurrentlyAborted) {
1286
+ // Record committed snapshots to message history for the final turn
1287
+ if (this.reversionManager) {
1288
+ const snapshots =
1289
+ this.reversionManager.getAndClearCommittedSnapshots();
1290
+ if (snapshots.length > 0) {
1291
+ this.messageManager.addFileHistoryBlock(snapshots);
1292
+ }
1293
+ }
1294
+
1295
+ // Goal evaluation — supersedes Stop hooks when active
1296
+ const goalManager = this.container.has("GoalManager")
1297
+ ? this.container.get<import("./goalManager.js").GoalManager>(
1298
+ "GoalManager",
1299
+ )
1300
+ : undefined;
1301
+
1302
+ let goalContinuing = false;
1303
+
1304
+ if (goalManager?.isGoalActive() && !this.subagentType) {
1305
+ // 1. Increment turn count and check circuit breakers
1306
+ goalManager.incrementTurnCount();
1307
+ const circuitBreaker = goalManager.checkCircuitBreakers();
1308
+
1309
+ if (circuitBreaker) {
1310
+ goalManager.clearGoal();
1311
+ logger?.info(`[Goal] ${circuitBreaker}`);
1312
+ this.messageManager.addUserMessage({
1313
+ content: `<system-reminder>${circuitBreaker}</system-reminder>`,
1314
+ isMeta: true,
1315
+ });
1316
+ // Fall through to normal Stop hooks on the final turn
1317
+ } else {
1318
+ // 2. Evaluate goal
1319
+ const evaluation = await goalManager.evaluateGoal(
1320
+ abortController.signal,
1321
+ );
1322
+
1323
+ if (evaluation.isMet) {
1324
+ goalManager.clearGoal();
1325
+ logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
1326
+ this.messageManager.addUserMessage({
1327
+ content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
1328
+ isMeta: true,
1329
+ });
1330
+ // Fall through to normal Stop hooks on the final turn
1331
+ } else {
1332
+ const goal = goalManager.getGoal()!;
1333
+ goal.lastReason = evaluation.reason;
1334
+ logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
1335
+ this.messageManager.addUserMessage({
1336
+ content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
1337
+ isMeta: true,
1338
+ });
1339
+ // Keep loading state active to prevent UI flicker
1340
+ this.setIsLoading(true);
1341
+ goalContinuing = true;
1342
+ // Restart outer loop to continue goal pursuit
1343
+ shouldRestart = true;
1344
+ turnOffset = 0;
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ // Skip Stop hooks when goal evaluator is continuing the conversation
1350
+ if (goalContinuing) {
1351
+ // Goal evaluator supersedes Stop hooks
1352
+ } else {
1353
+ const shouldContinue = await this.executeStopHooks();
1354
+
1355
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1356
+ // restart the AI conversation cycle
1357
+ if (shouldContinue) {
1358
+ logger?.info(
1359
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1360
+ );
1361
+
1362
+ // Restart the conversation to let AI fix the issues
1363
+ shouldRestart = true;
1364
+ turnOffset = 0;
1365
+ }
1366
+ }
1367
+ }
1368
+
1369
+ // Inject pending notifications from background tasks (after Stop hooks,
1370
+ // aligned with Claude Code which fires Stop hooks unconditionally)
1275
1371
  const notificationQueue = this.container.has("NotificationQueue")
1276
1372
  ? this.container.get<NotificationQueue>("NotificationQueue")
1277
1373
  : undefined;
@@ -1292,102 +1388,6 @@ export class AIManager {
1292
1388
  // Restart outer loop to process the notifications
1293
1389
  shouldRestart = true;
1294
1390
  turnOffset = 0;
1295
- } else {
1296
- // Clear temporary rules
1297
- this.permissionManager?.clearTemporaryRules();
1298
-
1299
- // Clear abort controllers
1300
- this.abortController = null;
1301
- this.toolAbortController = null;
1302
-
1303
- // Execute Stop/SubagentStop hooks only if the operation was not aborted
1304
- const isCurrentlyAborted =
1305
- abortController.signal.aborted ||
1306
- toolAbortController.signal.aborted;
1307
-
1308
- if (!isCurrentlyAborted) {
1309
- // Record committed snapshots to message history for the final turn
1310
- if (this.reversionManager) {
1311
- const snapshots =
1312
- this.reversionManager.getAndClearCommittedSnapshots();
1313
- if (snapshots.length > 0) {
1314
- this.messageManager.addFileHistoryBlock(snapshots);
1315
- }
1316
- }
1317
-
1318
- // Goal evaluation — supersedes Stop hooks when active
1319
- const goalManager = this.container.has("GoalManager")
1320
- ? this.container.get<import("./goalManager.js").GoalManager>(
1321
- "GoalManager",
1322
- )
1323
- : undefined;
1324
-
1325
- let goalContinuing = false;
1326
-
1327
- if (goalManager?.isGoalActive() && !this.subagentType) {
1328
- // 1. Increment turn count and check circuit breakers
1329
- goalManager.incrementTurnCount();
1330
- const circuitBreaker = goalManager.checkCircuitBreakers();
1331
-
1332
- if (circuitBreaker) {
1333
- goalManager.clearGoal();
1334
- logger?.info(`[Goal] ${circuitBreaker}`);
1335
- this.messageManager.addUserMessage({
1336
- content: `<system-reminder>${circuitBreaker}</system-reminder>`,
1337
- isMeta: true,
1338
- });
1339
- // Fall through to normal Stop hooks on the final turn
1340
- } else {
1341
- // 2. Evaluate goal
1342
- const evaluation = await goalManager.evaluateGoal(
1343
- abortController.signal,
1344
- );
1345
-
1346
- if (evaluation.isMet) {
1347
- goalManager.clearGoal();
1348
- logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
1349
- this.messageManager.addUserMessage({
1350
- content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
1351
- isMeta: true,
1352
- });
1353
- // Fall through to normal Stop hooks on the final turn
1354
- } else {
1355
- const goal = goalManager.getGoal()!;
1356
- goal.lastReason = evaluation.reason;
1357
- logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
1358
- this.messageManager.addUserMessage({
1359
- content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
1360
- isMeta: true,
1361
- });
1362
- // Keep loading state active to prevent UI flicker
1363
- this.setIsLoading(true);
1364
- goalContinuing = true;
1365
- // Restart outer loop to continue goal pursuit
1366
- shouldRestart = true;
1367
- turnOffset = 0;
1368
- }
1369
- }
1370
- }
1371
-
1372
- // Skip Stop hooks when goal evaluator is continuing the conversation
1373
- if (goalContinuing) {
1374
- // Goal evaluator supersedes Stop hooks
1375
- } else {
1376
- const shouldContinue = await this.executeStopHooks();
1377
-
1378
- // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1379
- // restart the AI conversation cycle
1380
- if (shouldContinue) {
1381
- logger?.info(
1382
- `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1383
- );
1384
-
1385
- // Restart the conversation to let AI fix the issues
1386
- shouldRestart = true;
1387
- turnOffset = 0;
1388
- }
1389
- }
1390
- }
1391
1391
  }
1392
1392
  }
1393
1393
 
@@ -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"],
@@ -133,6 +133,7 @@ export class OpenAIClient {
133
133
  }
134
134
  if (attempt < MAX_RETRIES) {
135
135
  logger.warn("OpenAI API network error, retrying...", {
136
+ model: params.model,
136
137
  attempt: attempt + 1,
137
138
  error: e,
138
139
  });
@@ -166,37 +167,18 @@ export class OpenAIClient {
166
167
  }
167
168
  }
168
169
 
169
- let errorBody: unknown;
170
+ let responseText = "";
170
171
  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
- }
172
+ responseText = await response.text();
181
173
  } catch (e) {
182
174
  logger.error("Failed to read error response text", { error: e });
183
- errorBody = {};
184
175
  }
185
176
 
186
177
  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,
178
+ `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}: ${responseText}`,
197
179
  ) as Error & { status?: number; body?: unknown };
198
180
  error.status = response.status;
199
- error.body = errorBody;
181
+ error.body = responseText;
200
182
 
201
183
  const retryableStatus =
202
184
  response.status === 429 ||
@@ -204,6 +186,7 @@ export class OpenAIClient {
204
186
  if (retryableStatus && attempt < MAX_RETRIES) {
205
187
  lastRetryAfter = response.headers.get("retry-after");
206
188
  logger.warn("OpenAI API error, retrying...", {
189
+ model: params.model,
207
190
  attempt: attempt + 1,
208
191
  status: response.status,
209
192
  retryAfter: lastRetryAfter,
@@ -215,7 +198,7 @@ export class OpenAIClient {
215
198
  logger.error("OpenAI API Error:", {
216
199
  status: response.status,
217
200
  statusText: response.statusText,
218
- errorBody,
201
+ body: responseText,
219
202
  });
220
203
  throw error;
221
204
  }
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
+ }