wave-agent-sdk 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +9 -6
- package/dist/agent.js +35 -33
- package/dist/builtin/index.d.ts +1 -0
- package/dist/builtin/index.js +20 -0
- package/dist/builtin/plugins.d.ts +1 -0
- package/dist/builtin/plugins.js +225 -0
- package/dist/builtin/skills/artifact.d.ts +1 -0
- package/dist/builtin/skills/artifact.js +18 -0
- package/dist/builtin/skills/code-review.d.ts +1 -0
- package/{builtin/skills/code-review/SKILL.md → dist/builtin/skills/code-review.js} +23 -19
- package/dist/builtin/skills/deep-research.d.ts +1 -0
- package/{builtin/skills/deep-research/SKILL.md → dist/builtin/skills/deep-research.js} +18 -14
- package/dist/builtin/skills/init.d.ts +1 -0
- package/{builtin/skills/init/SKILL.md → dist/builtin/skills/init.js} +6 -3
- package/dist/builtin/skills/loop.d.ts +1 -0
- package/dist/builtin/skills/loop.js +83 -0
- package/dist/builtin/skills/settings.d.ts +1 -0
- package/dist/builtin/skills/settings.js +1249 -0
- package/dist/builtin/skills/simplify.d.ts +1 -0
- package/{builtin/skills/simplify/SKILL.md → dist/builtin/skills/simplify.js} +7 -3
- package/dist/builtin/subagents.d.ts +1 -0
- package/dist/builtin/subagents.js +164 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +10 -0
- package/dist/managers/aiManager.js +49 -5
- package/dist/managers/mcpManager.js +1 -1
- package/dist/managers/messageManager.d.ts +6 -0
- package/dist/managers/messageManager.js +33 -0
- package/dist/managers/subagentManager.d.ts +9 -0
- package/dist/managers/subagentManager.js +62 -3
- package/dist/services/hook.js +41 -7
- package/dist/services/initializationService.js +0 -21
- package/dist/services/jsonlHandler.d.ts +37 -2
- package/dist/services/jsonlHandler.js +55 -6
- package/dist/services/session.d.ts +35 -4
- package/dist/services/session.js +233 -36
- package/dist/services/worktreeHooks.d.ts +45 -0
- package/dist/services/worktreeHooks.js +133 -0
- package/dist/tools/agentTool.js +36 -1
- package/dist/tools/bashTool.js +120 -57
- package/dist/tools/editTool.js +1 -0
- package/dist/tools/enterWorktreeTool.d.ts +1 -1
- package/dist/tools/enterWorktreeTool.js +44 -31
- package/dist/tools/exitWorktreeTool.js +21 -26
- package/dist/tools/readTool.js +6 -3
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/writeTool.js +1 -0
- package/dist/types/agent.d.ts +5 -0
- package/dist/types/hooks.d.ts +3 -2
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/skills.d.ts +0 -1
- package/dist/types/skills.js +0 -1
- package/dist/utils/asyncWorkRegistry.d.ts +32 -0
- package/dist/utils/asyncWorkRegistry.js +81 -0
- package/dist/utils/builtinEmbed.d.ts +21 -0
- package/dist/utils/builtinEmbed.js +53 -0
- package/dist/utils/configPaths.d.ts +0 -1
- package/dist/utils/configPaths.js +5 -26
- package/dist/utils/containerSetup.js +5 -0
- package/dist/utils/convertMessagesForAPI.js +21 -2
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/skillParser.js +3 -6
- package/dist/utils/toolImagePersistence.d.ts +25 -0
- package/dist/utils/toolImagePersistence.js +56 -0
- package/dist/utils/windowsPaths.d.ts +28 -0
- package/dist/utils/windowsPaths.js +47 -0
- package/dist/utils/worktreeSession.d.ts +6 -0
- package/dist/utils/worktreeUtils.d.ts +7 -0
- package/dist/utils/worktreeUtils.js +88 -40
- package/package.json +5 -5
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +0 -8
- package/builtin/plugins/sdd/hooks/hooks.json +0 -14
- package/builtin/plugins/sdd/scripts/session-start.js +0 -24
- package/builtin/plugins/sdd/scripts/spec-count.js +0 -77
- package/builtin/plugins/sdd/skills/specify/SKILL.md +0 -47
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +0 -47
- package/builtin/skills/artifact/SKILL.md +0 -14
- package/builtin/skills/loop/SKILL.md +0 -79
- package/builtin/skills/settings/ENV.md +0 -78
- package/builtin/skills/settings/HOOKS.md +0 -227
- package/builtin/skills/settings/MCP.md +0 -137
- package/builtin/skills/settings/MEMORY.md +0 -76
- package/builtin/skills/settings/MODELS.md +0 -119
- package/builtin/skills/settings/PERMISSIONS.md +0 -88
- package/builtin/skills/settings/PLUGINS.md +0 -171
- package/builtin/skills/settings/SKILL.md +0 -132
- package/builtin/skills/settings/SKILLS.md +0 -107
- package/builtin/skills/settings/SUBAGENTS.md +0 -77
- package/builtin/subagents/bash.md +0 -19
- package/builtin/subagents/explore.md +0 -43
- package/builtin/subagents/general-purpose.md +0 -20
- package/builtin/subagents/plan.md +0 -56
- package/builtin/subagents/vision.md +0 -18
package/dist/tools/bashTool.js
CHANGED
|
@@ -11,6 +11,14 @@ import { processToolResult } from "../utils/toolResultStorage.js";
|
|
|
11
11
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
12
12
|
import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
|
|
13
13
|
const BASH_DEFAULT_TIMEOUT_MS = 120000;
|
|
14
|
+
// After the shell exits, its last stdout/stderr chunks may still be in flight:
|
|
15
|
+
// Node emits the child's 'exit' event as soon as the process terminates, then
|
|
16
|
+
// delivers the remaining pipe data on a later event-loop turn and finally
|
|
17
|
+
// emits 'close'. Finalization therefore waits for the streams to 'end' before
|
|
18
|
+
// reading the buffers. This bound covers grandchildren that inherit the pipe
|
|
19
|
+
// write ends (e.g. `sleep 30 &`), which would delay 'end' indefinitely — in
|
|
20
|
+
// that case the shell's own output is returned, matching pre-existing behavior.
|
|
21
|
+
const SHELL_STREAM_FLUSH_TIMEOUT_MS = 100;
|
|
14
22
|
/**
|
|
15
23
|
* Wrap a user command so we can append CWD tracking (`&& pwd -P`) without the
|
|
16
24
|
* appended part being affected by trailing here-docs, unbalanced quotes, or
|
|
@@ -433,70 +441,125 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
433
441
|
updateRealtimeResults();
|
|
434
442
|
}
|
|
435
443
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
444
|
+
// The 'exit' event fires before the stdio pipes have fully drained:
|
|
445
|
+
// Node emits 'exit' as soon as the process terminates, then delivers the
|
|
446
|
+
// remaining pipe data on a later event-loop turn and finally emits
|
|
447
|
+
// 'close'. Reading the buffers at 'exit' could therefore drop the
|
|
448
|
+
// shell's trailing output, written just before it exited (observed as
|
|
449
|
+
// empty stdout with exit code 0 for a fast `echo` under load). The
|
|
450
|
+
// result is only finalized after the streams have emitted 'end'. A
|
|
451
|
+
// bounded valve covers grandchildren that inherit the pipe write ends
|
|
452
|
+
// (e.g. `sleep 30 &`), which would delay 'end' indefinitely — in that
|
|
453
|
+
// case the shell's own output is returned, matching pre-existing
|
|
454
|
+
// behavior.
|
|
455
|
+
const finalizeShellResult = (code) => {
|
|
456
|
+
// Streams that may still hold undelivered data: real pipe streams not
|
|
457
|
+
// yet at EOF. Mocked child_process tests provide plain objects without
|
|
458
|
+
// `once` that never emit 'end' — skip the flush wait for them so the
|
|
459
|
+
// exit path is unchanged.
|
|
460
|
+
const pendingStreams = [child.stdout, child.stderr].filter((stream) => !!stream &&
|
|
461
|
+
!stream.readableEnded &&
|
|
462
|
+
typeof stream.once === "function");
|
|
463
|
+
let finalized = false;
|
|
464
|
+
const finish = () => {
|
|
465
|
+
if (finalized)
|
|
466
|
+
return;
|
|
467
|
+
finalized = true;
|
|
468
|
+
clearTimeout(flushTimer);
|
|
469
|
+
completeShellResult(code);
|
|
470
|
+
};
|
|
471
|
+
if (pendingStreams.length === 0) {
|
|
472
|
+
completeShellResult(code);
|
|
473
|
+
return;
|
|
440
474
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
475
|
+
const remainingStreams = new Set(pendingStreams);
|
|
476
|
+
const onStreamDone = (stream) => {
|
|
477
|
+
remainingStreams.delete(stream);
|
|
478
|
+
if (remainingStreams.size === 0) {
|
|
479
|
+
finish();
|
|
444
480
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
481
|
+
};
|
|
482
|
+
for (const stream of pendingStreams) {
|
|
483
|
+
stream.once("end", () => onStreamDone(stream));
|
|
484
|
+
stream.once("error", () => onStreamDone(stream));
|
|
485
|
+
}
|
|
486
|
+
// Safety valve for the grandchild case. Runs via setImmediate (which
|
|
487
|
+
// executes after the next poll), so any data still in the kernel pipe
|
|
488
|
+
// is delivered before the buffers are read.
|
|
489
|
+
const flushTimer = setTimeout(() => {
|
|
490
|
+
if (!finalized) {
|
|
491
|
+
setImmediate(finish);
|
|
453
492
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
493
|
+
}, SHELL_STREAM_FLUSH_TIMEOUT_MS);
|
|
494
|
+
};
|
|
495
|
+
const completeShellResult = (code) => {
|
|
496
|
+
// Read the new CWD from the temporary file
|
|
497
|
+
let newCwd;
|
|
498
|
+
try {
|
|
499
|
+
if (fs.existsSync(tempCwdFile)) {
|
|
500
|
+
newCwd = fs.readFileSync(tempCwdFile, "utf8").trim();
|
|
501
|
+
// Validate the path exists before calling the callback
|
|
502
|
+
fs.accessSync(newCwd, fs.constants.F_OK);
|
|
457
503
|
}
|
|
458
|
-
|
|
459
|
-
|
|
504
|
+
}
|
|
505
|
+
catch (fileError) {
|
|
506
|
+
logger.warn(`Could not read or validate new CWD from temp file ${tempCwdFile}:`, fileError);
|
|
507
|
+
newCwd = undefined;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
cleanupTempFile();
|
|
511
|
+
}
|
|
512
|
+
// If CWD changed, call the onCwdChange callback and add notification
|
|
513
|
+
let cwdMessage;
|
|
514
|
+
if (newCwd && newCwd !== context.workdir && context.onCwdChange) {
|
|
515
|
+
const isInSafeZone = context.permissionManager?.isPathInSafeZone?.(newCwd) ?? true;
|
|
516
|
+
if (!isInSafeZone && context.originalWorkdir) {
|
|
517
|
+
context.onCwdChange(context.originalWorkdir);
|
|
518
|
+
cwdMessage = `Shell cwd was reset to ${context.originalWorkdir}`;
|
|
460
519
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const isInSafeZone = context.permissionManager?.isPathInSafeZone?.(newCwd) ?? true;
|
|
465
|
-
if (!isInSafeZone && context.originalWorkdir) {
|
|
466
|
-
context.onCwdChange(context.originalWorkdir);
|
|
467
|
-
cwdMessage = `Shell cwd was reset to ${context.originalWorkdir}`;
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
context.onCwdChange(newCwd);
|
|
471
|
-
cwdMessage = `Shell working directory changed to ${newCwd}`;
|
|
472
|
-
}
|
|
520
|
+
else {
|
|
521
|
+
context.onCwdChange(newCwd);
|
|
522
|
+
cwdMessage = `Shell working directory changed to ${newCwd}`;
|
|
473
523
|
}
|
|
474
|
-
const exitCode = code ?? 0;
|
|
475
|
-
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
476
|
-
// character split across the last chunk).
|
|
477
|
-
if (stdoutDecoder)
|
|
478
|
-
outputBuffer += stdoutDecoder.flush();
|
|
479
|
-
if (stderrDecoder)
|
|
480
|
-
errorBuffer += stderrDecoder.flush();
|
|
481
|
-
const combinedOutput = outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
482
|
-
// Prepend CWD change message to output if present
|
|
483
|
-
const finalOutput = recoveryNotice +
|
|
484
|
-
(cwdMessage ? cwdMessage + "\n" : "") +
|
|
485
|
-
(combinedOutput || `Command executed with exit code: ${exitCode}`);
|
|
486
|
-
const content = processToolResult(finalOutput, BASH_MAX_OUTPUT_CHARS, "bash");
|
|
487
|
-
const lines = combinedOutput.trim().split("\n");
|
|
488
|
-
const shortResult = lines.length <= 3
|
|
489
|
-
? lines.join("\n")
|
|
490
|
-
: `... +${lines.length - 3} lines\n` + lines.slice(-3).join("\n");
|
|
491
|
-
resolve({
|
|
492
|
-
success: exitCode === 0,
|
|
493
|
-
content,
|
|
494
|
-
shortResult: shortResult || undefined,
|
|
495
|
-
error: exitCode !== 0
|
|
496
|
-
? `Command failed with exit code: ${exitCode}`
|
|
497
|
-
: undefined,
|
|
498
|
-
});
|
|
499
524
|
}
|
|
525
|
+
const exitCode = code ?? 0;
|
|
526
|
+
// Decode any bytes still held at stream end (e.g. a trailing UTF-8
|
|
527
|
+
// character split across the last chunk).
|
|
528
|
+
if (stdoutDecoder)
|
|
529
|
+
outputBuffer += stdoutDecoder.flush();
|
|
530
|
+
if (stderrDecoder)
|
|
531
|
+
errorBuffer += stderrDecoder.flush();
|
|
532
|
+
const combinedOutput = outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
533
|
+
// Prepend CWD change message to output if present
|
|
534
|
+
const finalOutput = recoveryNotice +
|
|
535
|
+
(cwdMessage ? cwdMessage + "\n" : "") +
|
|
536
|
+
(combinedOutput || `Command executed with exit code: ${exitCode}`);
|
|
537
|
+
const content = processToolResult(finalOutput, BASH_MAX_OUTPUT_CHARS, "bash");
|
|
538
|
+
const lines = combinedOutput.trim().split("\n");
|
|
539
|
+
const shortResult = lines.length <= 3
|
|
540
|
+
? lines.join("\n")
|
|
541
|
+
: `... +${lines.length - 3} lines\n` + lines.slice(-3).join("\n");
|
|
542
|
+
resolve({
|
|
543
|
+
success: exitCode === 0,
|
|
544
|
+
content,
|
|
545
|
+
shortResult: shortResult || undefined,
|
|
546
|
+
error: exitCode !== 0
|
|
547
|
+
? `Command failed with exit code: ${exitCode}`
|
|
548
|
+
: undefined,
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
child.on("exit", (code) => {
|
|
552
|
+
isFinished = true;
|
|
553
|
+
if (context.foregroundTaskManager) {
|
|
554
|
+
context.foregroundTaskManager.unregisterForegroundTask(foregroundTaskId);
|
|
555
|
+
}
|
|
556
|
+
if (isAborted || isBackgrounded) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (timeoutHandle) {
|
|
560
|
+
clearTimeout(timeoutHandle);
|
|
561
|
+
}
|
|
562
|
+
finalizeShellResult(code);
|
|
500
563
|
});
|
|
501
564
|
child.on("error", (error) => {
|
|
502
565
|
isFinished = true;
|
package/dist/tools/editTool.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Mirrors Claude Code's EnterWorktree tool behavior and prompt.
|
|
4
4
|
*/
|
|
5
5
|
import type { ToolPlugin } from "./types.js";
|
|
6
|
-
export declare const ENTER_WORKTREE_TOOL_PROMPT = "Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.\n\n## When to Use\n\n- The user explicitly says \"worktree\" (e.g., \"start a worktree\", \"work in a worktree\", \"create a worktree\", \"use a worktree\")\n\n## When NOT to Use\n\n- The user asks to create a branch, switch branches, or work on a different branch \u2014 use git commands instead\n- The user asks to fix a bug or work on a feature \u2014 use normal git workflow unless they specifically mention worktrees\n- Never use this tool unless the user explicitly mentions \"worktree\"\n\n## Requirements\n\n- Must be in a git repository
|
|
6
|
+
export declare const ENTER_WORKTREE_TOOL_PROMPT = "Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.\n\n## When to Use\n\n- The user explicitly says \"worktree\" (e.g., \"start a worktree\", \"work in a worktree\", \"create a worktree\", \"use a worktree\")\n\n## When NOT to Use\n\n- The user asks to create a branch, switch branches, or work on a different branch \u2014 use git commands instead\n- The user asks to fix a bug or work on a feature \u2014 use normal git workflow unless they specifically mention worktrees\n- Never use this tool unless the user explicitly mentions \"worktree\"\n\n## Requirements\n\n- Must be in a git repository (or have a WorktreeCreate hook configured, in which case the hook creates the worktree)\n\n## Behavior\n\n- Creates a new git worktree inside `.wave/worktrees/` with a new branch based on HEAD\n- When a WorktreeCreate hook is configured, the hook creates the worktree instead (its stdout provides the worktree path) and no git command runs\n- Switches the session's working directory to the new worktree\n- Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it\n\n## Parameters\n\n- `name` (optional): A name for the worktree. Each \"/\"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.\n";
|
|
7
7
|
export declare const enterWorktreeTool: ToolPlugin;
|
|
@@ -6,6 +6,7 @@ import { createWorktree, validateWorktreeName, generateWorktreeName, performPost
|
|
|
6
6
|
import { getGitMainRepoRoot } from "../utils/gitUtils.js";
|
|
7
7
|
import { ENTER_WORKTREE_TOOL_NAME } from "../constants/tools.js";
|
|
8
8
|
import { logger } from "../utils/globalLogger.js";
|
|
9
|
+
import { executeWorktreeCreateHook, hasWorktreeCreateHook, } from "../services/worktreeHooks.js";
|
|
9
10
|
export const ENTER_WORKTREE_TOOL_PROMPT = `Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
|
|
10
11
|
|
|
11
12
|
## When to Use
|
|
@@ -20,12 +21,12 @@ export const ENTER_WORKTREE_TOOL_PROMPT = `Use this tool ONLY when the user expl
|
|
|
20
21
|
|
|
21
22
|
## Requirements
|
|
22
23
|
|
|
23
|
-
- Must be in a git repository
|
|
24
|
-
- Must not already be in a worktree
|
|
24
|
+
- Must be in a git repository (or have a WorktreeCreate hook configured, in which case the hook creates the worktree)
|
|
25
25
|
|
|
26
26
|
## Behavior
|
|
27
27
|
|
|
28
28
|
- Creates a new git worktree inside \`.wave/worktrees/\` with a new branch based on HEAD
|
|
29
|
+
- When a WorktreeCreate hook is configured, the hook creates the worktree instead (its stdout provides the worktree path) and no git command runs
|
|
29
30
|
- Switches the session's working directory to the new worktree
|
|
30
31
|
- Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
|
|
31
32
|
|
|
@@ -73,7 +74,46 @@ export const enterWorktreeTool = {
|
|
|
73
74
|
error: `Invalid worktree name: ${e.message}`,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
|
-
//
|
|
77
|
+
// Hook-based creation first (allows user-configured VCS, including non-git)
|
|
78
|
+
const hookConfiguration = context.hookManager?.getConfiguration();
|
|
79
|
+
if (hasWorktreeCreateHook(hookConfiguration)) {
|
|
80
|
+
try {
|
|
81
|
+
const { worktreePath } = await executeWorktreeCreateHook(name, hookConfiguration, {
|
|
82
|
+
projectDir: context.workdir,
|
|
83
|
+
sessionId: context.sessionId ?? "",
|
|
84
|
+
transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
|
|
85
|
+
env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
|
|
86
|
+
});
|
|
87
|
+
// Hook-based worktrees skip post-creation setup; the hook script owns
|
|
88
|
+
// initialization. repoRoot falls back to the workdir for non-git repos.
|
|
89
|
+
const hookBasedSession = {
|
|
90
|
+
originalCwd: context.workdir,
|
|
91
|
+
worktreePath,
|
|
92
|
+
worktreeBranch: "",
|
|
93
|
+
worktreeName: name,
|
|
94
|
+
isNew: true,
|
|
95
|
+
repoRoot: getGitMainRepoRoot(context.workdir) ?? context.workdir,
|
|
96
|
+
hookBased: true,
|
|
97
|
+
};
|
|
98
|
+
const aiManager = context.aiManager;
|
|
99
|
+
if (aiManager) {
|
|
100
|
+
aiManager.setWorktreeSession(hookBasedSession);
|
|
101
|
+
aiManager.setWorkdir(worktreePath);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
content: `Created worktree at ${worktreePath}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted. WorktreeCreate hooks were executed.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
content: `Failed to create worktree: ${error.message}`,
|
|
112
|
+
error: "WorktreeCreate hook failed",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Git-based fallback
|
|
77
117
|
const mainRepoRoot = getGitMainRepoRoot(context.workdir);
|
|
78
118
|
if (!mainRepoRoot) {
|
|
79
119
|
return {
|
|
@@ -112,39 +152,12 @@ export const enterWorktreeTool = {
|
|
|
112
152
|
aiManager.setWorktreeSession(session);
|
|
113
153
|
aiManager.setWorkdir(worktreeInfo.path);
|
|
114
154
|
}
|
|
115
|
-
// Trigger WorktreeCreate hook if worktree is new
|
|
116
|
-
let hookTriggered = false;
|
|
117
|
-
if (session.isNew && context.hookManager) {
|
|
118
|
-
try {
|
|
119
|
-
const hookResults = await context.hookManager.executeHooks("WorktreeCreate", {
|
|
120
|
-
event: "WorktreeCreate",
|
|
121
|
-
projectDir: worktreeInfo.path,
|
|
122
|
-
timestamp: new Date(),
|
|
123
|
-
sessionId: context.sessionId ?? "",
|
|
124
|
-
transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
|
|
125
|
-
cwd: worktreeInfo.path,
|
|
126
|
-
worktreeName: worktreeInfo.name,
|
|
127
|
-
env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
|
|
128
|
-
});
|
|
129
|
-
if (context.messageManager) {
|
|
130
|
-
context.hookManager.processHookResults("WorktreeCreate", hookResults, context.messageManager);
|
|
131
|
-
}
|
|
132
|
-
hookTriggered = true;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
// Non-blocking: log but don't fail the tool
|
|
136
|
-
logger?.warn("WorktreeCreate hooks execution failed:", error);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
155
|
const branchInfo = worktreeInfo.branch
|
|
140
156
|
? ` on branch ${worktreeInfo.branch}`
|
|
141
157
|
: "";
|
|
142
|
-
const hookInfo = hookTriggered
|
|
143
|
-
? " WorktreeCreate hooks were executed."
|
|
144
|
-
: "";
|
|
145
158
|
return {
|
|
146
159
|
success: true,
|
|
147
|
-
content: `Created worktree at ${worktreeInfo.path}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted
|
|
160
|
+
content: `Created worktree at ${worktreeInfo.path}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.`,
|
|
148
161
|
};
|
|
149
162
|
},
|
|
150
163
|
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { removeWorktree, countWorktreeChanges, } from "../utils/worktreeUtils.js";
|
|
6
6
|
import { EXIT_WORKTREE_TOOL_NAME } from "../constants/tools.js";
|
|
7
7
|
import { logger } from "../utils/globalLogger.js";
|
|
8
|
+
import { executeWorktreeRemoveHook } from "../services/worktreeHooks.js";
|
|
8
9
|
export const EXIT_WORKTREE_TOOL_PROMPT = `Exit a worktree session created by EnterWorktree and return the session to the original working directory.
|
|
9
10
|
|
|
10
11
|
## Scope
|
|
@@ -128,32 +129,29 @@ export const exitWorktreeTool = {
|
|
|
128
129
|
};
|
|
129
130
|
// Count changes BEFORE removing the worktree (directory will be gone after)
|
|
130
131
|
const summary = countWorktreeChanges(worktreePath, session.originalHeadCommit) ?? { changedFiles: 0, commits: 0 };
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
|
|
145
|
-
});
|
|
146
|
-
if (context.messageManager) {
|
|
147
|
-
context.hookManager.processHookResults("WorktreeRemove", hookResults, context.messageManager);
|
|
148
|
-
}
|
|
149
|
-
hookTriggered = true;
|
|
132
|
+
let hookNote = "";
|
|
133
|
+
if (session.hookBased) {
|
|
134
|
+
// Hook-based worktree: delegate removal to the WorktreeRemove hook.
|
|
135
|
+
// wave never runs `git worktree remove` for hook-based worktrees.
|
|
136
|
+
const configuration = context.hookManager?.getConfiguration();
|
|
137
|
+
const hookRan = await executeWorktreeRemoveHook(worktreePath, configuration, {
|
|
138
|
+
projectDir: originalCwd,
|
|
139
|
+
sessionId: context.sessionId ?? "",
|
|
140
|
+
transcriptPath: context.messageManager?.getTranscriptPath?.() ?? "",
|
|
141
|
+
env: Object.fromEntries(Object.entries(context.sessionEnv ?? process.env).filter((e) => e[1] !== undefined)),
|
|
142
|
+
});
|
|
143
|
+
if (hookRan) {
|
|
144
|
+
hookNote = " WorktreeRemove hooks were executed.";
|
|
150
145
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
else {
|
|
147
|
+
logger?.warn(`No WorktreeRemove hook configured, hook-based worktree left at: ${worktreePath}`);
|
|
148
|
+
hookNote =
|
|
149
|
+
" No WorktreeRemove hook configured, hook-based worktree left at the path above.";
|
|
154
150
|
}
|
|
155
151
|
}
|
|
156
|
-
|
|
152
|
+
else {
|
|
153
|
+
removeWorktree(worktreeInfo);
|
|
154
|
+
}
|
|
157
155
|
// Clear session state and restore CWD
|
|
158
156
|
const aiManager = context.aiManager;
|
|
159
157
|
if (aiManager) {
|
|
@@ -170,9 +168,6 @@ export const exitWorktreeTool = {
|
|
|
170
168
|
const discardNote = discardParts.length > 0
|
|
171
169
|
? ` Discarded ${discardParts.join(" and ")}.`
|
|
172
170
|
: "";
|
|
173
|
-
const hookNote = hookTriggered
|
|
174
|
-
? " WorktreeRemove hooks were executed."
|
|
175
|
-
: "";
|
|
176
171
|
return {
|
|
177
172
|
success: true,
|
|
178
173
|
content: `Exited and removed worktree at ${worktreePath}.${discardNote} Session is now back in ${originalCwd}.${hookNote}`,
|
package/dist/tools/readTool.js
CHANGED
|
@@ -208,12 +208,14 @@ Usage:
|
|
|
208
208
|
: resolvePath(filePath, context.workdir);
|
|
209
209
|
const stats = await stat(actualFilePath);
|
|
210
210
|
// Deduplication: only dedup if the exact same range was read before
|
|
211
|
+
// (aligned with Claude Code — full reads dedup too). Entries written by
|
|
212
|
+
// Edit/Write have source "edit"/"write" and never dedup, so a file that
|
|
213
|
+
// was written before its first Read is still returned in full.
|
|
211
214
|
if (context.readFileState) {
|
|
212
215
|
const state = context.readFileState.get(actualFilePath);
|
|
213
|
-
// Only dedup entries from a prior Read with explicit offset (not Edit/Write entries)
|
|
214
216
|
if (state &&
|
|
215
|
-
state.
|
|
216
|
-
state.
|
|
217
|
+
state.source === "read" &&
|
|
218
|
+
state.mtime === stats.mtime.getTime()) {
|
|
217
219
|
const rangeMatch = state.offset === offset && state.limit === limit;
|
|
218
220
|
if (rangeMatch) {
|
|
219
221
|
return {
|
|
@@ -249,6 +251,7 @@ Usage:
|
|
|
249
251
|
context.readFileState.set(actualFilePath, {
|
|
250
252
|
mtime: stats.mtime.getTime(),
|
|
251
253
|
hash,
|
|
254
|
+
source: "read",
|
|
252
255
|
offset, // undefined for full reads
|
|
253
256
|
limit, // undefined for full reads
|
|
254
257
|
});
|
package/dist/tools/types.d.ts
CHANGED
package/dist/tools/writeTool.js
CHANGED
package/dist/types/agent.d.ts
CHANGED
|
@@ -106,4 +106,9 @@ export interface AgentCallbacks extends MessageManagerCallbacks, BackgroundTaskM
|
|
|
106
106
|
onCommandRunningChange?: (running: boolean) => void;
|
|
107
107
|
onWorkdirChange?: (newCwd: string) => void;
|
|
108
108
|
onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
|
|
109
|
+
/** Streaming content from the compaction fork (accumulated). Reasoning
|
|
110
|
+
* chunks fall back to this channel when no reasoning callback is provided. */
|
|
111
|
+
onCompactionContentUpdate?: (content: string) => void;
|
|
112
|
+
/** Streaming reasoning from the compaction fork (accumulated). */
|
|
113
|
+
onCompactionReasoningUpdate?: (content: string) => void;
|
|
109
114
|
}
|
package/dist/types/hooks.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface HookExecutionContext {
|
|
|
22
22
|
toolName?: string;
|
|
23
23
|
projectDir: string;
|
|
24
24
|
timestamp: Date;
|
|
25
|
-
|
|
25
|
+
name?: string;
|
|
26
26
|
worktreePath?: string;
|
|
27
27
|
planFilePath?: string;
|
|
28
28
|
}
|
|
@@ -33,6 +33,8 @@ export interface HookExecutionResult {
|
|
|
33
33
|
stderr?: string;
|
|
34
34
|
duration: number;
|
|
35
35
|
timedOut: boolean;
|
|
36
|
+
/** The executed command, tagged onto results by callers that iterate multiple hooks */
|
|
37
|
+
command?: string;
|
|
36
38
|
}
|
|
37
39
|
export interface HookExecutionOptions {
|
|
38
40
|
timeout?: number;
|
|
@@ -107,7 +109,6 @@ export interface ExtendedHookExecutionContext extends HookExecutionContext {
|
|
|
107
109
|
env?: Record<string, string>;
|
|
108
110
|
userPrompt?: string;
|
|
109
111
|
subagentType?: string;
|
|
110
|
-
worktreeName?: string;
|
|
111
112
|
oldCwd?: string;
|
|
112
113
|
newCwd?: string;
|
|
113
114
|
source?: SessionStartSource;
|
package/dist/types/skills.d.ts
CHANGED
|
@@ -85,7 +85,6 @@ export declare const SKILL_DEFAULTS: {
|
|
|
85
85
|
readonly PROJECT_SKILLS_DIR: ".wave/skills";
|
|
86
86
|
readonly SKILL_FILE_NAME: "SKILL.md";
|
|
87
87
|
readonly MAX_NAME_LENGTH: 64;
|
|
88
|
-
readonly MAX_DESCRIPTION_LENGTH: 1024;
|
|
89
88
|
readonly MIN_DESCRIPTION_LENGTH: 1;
|
|
90
89
|
readonly NAME_PATTERN: RegExp;
|
|
91
90
|
readonly MAX_METADATA_CACHE: 1000;
|
package/dist/types/skills.js
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of live async work that must drain before the agent is destroyed.
|
|
3
|
+
*
|
|
4
|
+
* Fire-and-forget work (dispatch, background subagents, fork subagents)
|
|
5
|
+
* registers its promise here so destroy() can wait deterministically instead
|
|
6
|
+
* of silently abandoning it (the "ghost work" class of #1808). The registry
|
|
7
|
+
* swallows rejections so an unregistered fire-and-forget caller can't turn a
|
|
8
|
+
* settled-then-removed promise into an unhandled rejection.
|
|
9
|
+
*/
|
|
10
|
+
export declare class AsyncWorkRegistry {
|
|
11
|
+
private readonly drainTimeoutMs;
|
|
12
|
+
private work;
|
|
13
|
+
private waiters;
|
|
14
|
+
constructor(drainTimeoutMs?: number);
|
|
15
|
+
get size(): number;
|
|
16
|
+
isEmpty(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Register a promise as live work. The promise is removed when it settles
|
|
19
|
+
* (fulfilled or rejected); rejections are consumed here so tracking never
|
|
20
|
+
* produces an unhandled rejection. Returns the original promise unchanged
|
|
21
|
+
* so the caller can still await it.
|
|
22
|
+
*/
|
|
23
|
+
track<T>(promise: Promise<T>): Promise<T>;
|
|
24
|
+
private remove;
|
|
25
|
+
/**
|
|
26
|
+
* Wait until the registry is empty, or until the timeout elapses. Loops
|
|
27
|
+
* until empty: work registered after an await point is still drained.
|
|
28
|
+
* Returns true if drained, false on timeout (leftover work is no longer
|
|
29
|
+
* lifecycle-managed).
|
|
30
|
+
*/
|
|
31
|
+
drain(timeoutMs?: number): Promise<boolean>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of live async work that must drain before the agent is destroyed.
|
|
3
|
+
*
|
|
4
|
+
* Fire-and-forget work (dispatch, background subagents, fork subagents)
|
|
5
|
+
* registers its promise here so destroy() can wait deterministically instead
|
|
6
|
+
* of silently abandoning it (the "ghost work" class of #1808). The registry
|
|
7
|
+
* swallows rejections so an unregistered fire-and-forget caller can't turn a
|
|
8
|
+
* settled-then-removed promise into an unhandled rejection.
|
|
9
|
+
*/
|
|
10
|
+
export class AsyncWorkRegistry {
|
|
11
|
+
constructor(drainTimeoutMs = 10000) {
|
|
12
|
+
this.drainTimeoutMs = drainTimeoutMs;
|
|
13
|
+
this.work = new Set();
|
|
14
|
+
this.waiters = [];
|
|
15
|
+
}
|
|
16
|
+
get size() {
|
|
17
|
+
return this.work.size;
|
|
18
|
+
}
|
|
19
|
+
isEmpty() {
|
|
20
|
+
return this.work.size === 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Register a promise as live work. The promise is removed when it settles
|
|
24
|
+
* (fulfilled or rejected); rejections are consumed here so tracking never
|
|
25
|
+
* produces an unhandled rejection. Returns the original promise unchanged
|
|
26
|
+
* so the caller can still await it.
|
|
27
|
+
*/
|
|
28
|
+
track(promise) {
|
|
29
|
+
this.work.add(promise);
|
|
30
|
+
promise.then(() => this.remove(promise), () => this.remove(promise));
|
|
31
|
+
return promise;
|
|
32
|
+
}
|
|
33
|
+
remove(promise) {
|
|
34
|
+
this.work.delete(promise);
|
|
35
|
+
if (this.work.size === 0) {
|
|
36
|
+
const waiters = this.waiters;
|
|
37
|
+
this.waiters = [];
|
|
38
|
+
for (const resolve of waiters) {
|
|
39
|
+
resolve();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wait until the registry is empty, or until the timeout elapses. Loops
|
|
45
|
+
* until empty: work registered after an await point is still drained.
|
|
46
|
+
* Returns true if drained, false on timeout (leftover work is no longer
|
|
47
|
+
* lifecycle-managed).
|
|
48
|
+
*/
|
|
49
|
+
async drain(timeoutMs = this.drainTimeoutMs) {
|
|
50
|
+
const deadline = Date.now() + timeoutMs;
|
|
51
|
+
while (this.work.size > 0) {
|
|
52
|
+
const remaining = deadline - Date.now();
|
|
53
|
+
if (remaining <= 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// Wait for the registry to empty, or for the timeout — whichever comes
|
|
57
|
+
// first (a tracked promise may never settle).
|
|
58
|
+
let waiter;
|
|
59
|
+
const notified = new Promise((resolve) => {
|
|
60
|
+
waiter = resolve;
|
|
61
|
+
this.waiters.push(resolve);
|
|
62
|
+
});
|
|
63
|
+
let timer;
|
|
64
|
+
const timedOut = new Promise((resolve) => {
|
|
65
|
+
timer = setTimeout(resolve, remaining);
|
|
66
|
+
});
|
|
67
|
+
await Promise.race([notified, timedOut]);
|
|
68
|
+
if (timer)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
// If the timer won, the work set is still non-empty: drop our waiter so
|
|
71
|
+
// a later drain isn't woken by a stale notification. If notified won,
|
|
72
|
+
// remove() already cleared the waiters array.
|
|
73
|
+
if (waiter && this.work.size > 0) {
|
|
74
|
+
const idx = this.waiters.indexOf(waiter);
|
|
75
|
+
if (idx >= 0)
|
|
76
|
+
this.waiters.splice(idx, 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtin content embedding.
|
|
3
|
+
*
|
|
4
|
+
* Builtin skills/subagents/plugins ship as TS source (src/builtin/*.ts) and are
|
|
5
|
+
* compiled into the bundle. At runtime they are lazily materialized to a
|
|
6
|
+
* content-hashed cache directory under the OS temp dir, so consumers that
|
|
7
|
+
* readdirSync real directories keep working unchanged — including inside
|
|
8
|
+
* esbuild bundles that never ship a `builtin/` dir on disk.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Content-hash key: sha256 of the sorted `rel\0content` entries, truncated.
|
|
12
|
+
* Any change to builtin content yields a new key, auto-invalidating stale caches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getBuiltinCacheKey(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Materialize builtin content to `{tmpdir}/wave-builtin/{contentHash}/` and
|
|
17
|
+
* return the directory. Memoized per process; idempotent across processes via
|
|
18
|
+
* the `.wave-builtin-complete` marker written last (a torn write leaves no
|
|
19
|
+
* marker, so the next run rewrites).
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureBuiltinMaterialized(): string;
|