wave-agent-sdk 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/builtin/skills/loop/SKILL.md +29 -3
  2. package/dist/agent.d.ts +11 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +44 -11
  5. package/dist/constants/tools.d.ts +3 -0
  6. package/dist/constants/tools.d.ts.map +1 -1
  7. package/dist/constants/tools.js +3 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts +13 -1
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +69 -17
  14. package/dist/managers/hookManager.d.ts.map +1 -1
  15. package/dist/managers/hookManager.js +9 -0
  16. package/dist/managers/mcpManager.d.ts +4 -1
  17. package/dist/managers/mcpManager.d.ts.map +1 -1
  18. package/dist/managers/mcpManager.js +25 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +7 -6
  21. package/dist/managers/permissionManager.d.ts +0 -2
  22. package/dist/managers/permissionManager.d.ts.map +1 -1
  23. package/dist/managers/permissionManager.js +0 -30
  24. package/dist/managers/slashCommandManager.d.ts +1 -0
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +20 -4
  27. package/dist/managers/subagentManager.d.ts +6 -1
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +17 -18
  30. package/dist/managers/toolManager.d.ts +6 -0
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/managers/toolManager.js +41 -1
  33. package/dist/prompts/index.d.ts +1 -2
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +14 -6
  36. package/dist/services/initializationService.d.ts +0 -2
  37. package/dist/services/initializationService.d.ts.map +1 -1
  38. package/dist/services/initializationService.js +3 -35
  39. package/dist/services/jsonlHandler.d.ts +4 -4
  40. package/dist/services/jsonlHandler.d.ts.map +1 -1
  41. package/dist/services/jsonlHandler.js +4 -13
  42. package/dist/services/memory.d.ts +6 -0
  43. package/dist/services/memory.d.ts.map +1 -1
  44. package/dist/services/memory.js +27 -14
  45. package/dist/services/session.d.ts.map +1 -1
  46. package/dist/services/session.js +3 -12
  47. package/dist/tools/agentTool.d.ts.map +1 -1
  48. package/dist/tools/agentTool.js +16 -4
  49. package/dist/tools/bashTool.d.ts.map +1 -1
  50. package/dist/tools/bashTool.js +2 -5
  51. package/dist/tools/cronCreateTool.d.ts.map +1 -1
  52. package/dist/tools/cronCreateTool.js +71 -6
  53. package/dist/tools/cronDeleteTool.d.ts.map +1 -1
  54. package/dist/tools/cronDeleteTool.js +5 -1
  55. package/dist/tools/cronListTool.d.ts.map +1 -1
  56. package/dist/tools/cronListTool.js +5 -1
  57. package/dist/tools/enterWorktreeTool.d.ts +8 -0
  58. package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
  59. package/dist/tools/enterWorktreeTool.js +144 -0
  60. package/dist/tools/exitWorktreeTool.d.ts +8 -0
  61. package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
  62. package/dist/tools/exitWorktreeTool.js +184 -0
  63. package/dist/tools/skillTool.d.ts.map +1 -1
  64. package/dist/tools/skillTool.js +16 -4
  65. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  66. package/dist/tools/taskManagementTools.js +4 -0
  67. package/dist/tools/toolSearchTool.d.ts +15 -0
  68. package/dist/tools/toolSearchTool.d.ts.map +1 -0
  69. package/dist/tools/toolSearchTool.js +185 -0
  70. package/dist/tools/types.d.ts +19 -0
  71. package/dist/tools/types.d.ts.map +1 -1
  72. package/dist/tools/webFetchTool.d.ts.map +1 -1
  73. package/dist/tools/webFetchTool.js +1 -0
  74. package/dist/types/agent.d.ts +6 -1
  75. package/dist/types/agent.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +3 -1
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/hooks.js +1 -0
  79. package/dist/types/messaging.d.ts +1 -0
  80. package/dist/types/messaging.d.ts.map +1 -1
  81. package/dist/types/session.d.ts +0 -4
  82. package/dist/types/session.d.ts.map +1 -1
  83. package/dist/utils/containerSetup.d.ts.map +1 -1
  84. package/dist/utils/containerSetup.js +4 -6
  85. package/dist/utils/cronToHuman.d.ts +6 -0
  86. package/dist/utils/cronToHuman.d.ts.map +1 -0
  87. package/dist/utils/cronToHuman.js +79 -0
  88. package/dist/utils/isDeferredTool.d.ts +19 -0
  89. package/dist/utils/isDeferredTool.d.ts.map +1 -0
  90. package/dist/utils/isDeferredTool.js +31 -0
  91. package/dist/utils/mcpUtils.d.ts.map +1 -1
  92. package/dist/utils/mcpUtils.js +1 -0
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/messageOperations.js +5 -0
  95. package/dist/utils/parseCronExpression.d.ts +6 -0
  96. package/dist/utils/parseCronExpression.d.ts.map +1 -0
  97. package/dist/utils/parseCronExpression.js +74 -0
  98. package/dist/utils/worktreeSession.d.ts +26 -0
  99. package/dist/utils/worktreeSession.d.ts.map +1 -0
  100. package/dist/utils/worktreeSession.js +14 -0
  101. package/dist/utils/worktreeUtils.d.ts +42 -0
  102. package/dist/utils/worktreeUtils.d.ts.map +1 -0
  103. package/dist/utils/worktreeUtils.js +236 -0
  104. package/package.json +1 -1
  105. package/src/agent.ts +61 -12
  106. package/src/constants/tools.ts +3 -0
  107. package/src/index.ts +1 -0
  108. package/src/managers/aiManager.ts +73 -18
  109. package/src/managers/hookManager.ts +10 -0
  110. package/src/managers/mcpManager.ts +32 -6
  111. package/src/managers/messageManager.ts +7 -8
  112. package/src/managers/permissionManager.ts +0 -42
  113. package/src/managers/slashCommandManager.ts +30 -5
  114. package/src/managers/subagentManager.ts +28 -23
  115. package/src/managers/toolManager.ts +47 -1
  116. package/src/prompts/index.ts +17 -6
  117. package/src/services/initializationService.ts +2 -41
  118. package/src/services/jsonlHandler.ts +12 -24
  119. package/src/services/memory.ts +30 -17
  120. package/src/services/session.ts +3 -14
  121. package/src/tools/agentTool.ts +24 -5
  122. package/src/tools/bashTool.ts +2 -5
  123. package/src/tools/cronCreateTool.ts +81 -8
  124. package/src/tools/cronDeleteTool.ts +7 -2
  125. package/src/tools/cronListTool.ts +7 -2
  126. package/src/tools/enterWorktreeTool.ts +183 -0
  127. package/src/tools/exitWorktreeTool.ts +242 -0
  128. package/src/tools/skillTool.ts +24 -4
  129. package/src/tools/taskManagementTools.ts +4 -0
  130. package/src/tools/toolSearchTool.ts +228 -0
  131. package/src/tools/types.ts +19 -0
  132. package/src/tools/webFetchTool.ts +1 -0
  133. package/src/types/agent.ts +6 -0
  134. package/src/types/hooks.ts +4 -0
  135. package/src/types/messaging.ts +1 -0
  136. package/src/types/session.ts +0 -8
  137. package/src/utils/containerSetup.ts +7 -8
  138. package/src/utils/cronToHuman.ts +99 -0
  139. package/src/utils/isDeferredTool.ts +36 -0
  140. package/src/utils/mcpUtils.ts +1 -0
  141. package/src/utils/messageOperations.ts +5 -0
  142. package/src/utils/parseCronExpression.ts +78 -0
  143. package/src/utils/worktreeSession.ts +36 -0
  144. package/src/utils/worktreeUtils.ts +288 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Git worktree creation and removal utilities for the SDK.
3
+ * Used by EnterWorktree and ExitWorktree tools.
4
+ */
5
+ import { execSync } from "node:child_process";
6
+ import * as path from "node:path";
7
+ import * as fs from "node:fs";
8
+ import { getGitMainRepoRoot, getDefaultRemoteBranch } from "./gitUtils.js";
9
+ import { logger } from "./globalLogger.js";
10
+ /**
11
+ * Validate a worktree name to prevent path traversal and invalid characters.
12
+ */
13
+ export function validateWorktreeName(name) {
14
+ const MAX_LENGTH = 64;
15
+ if (name.length > MAX_LENGTH) {
16
+ throw new Error(`Invalid worktree name: must be ${MAX_LENGTH} characters or fewer (got ${name.length})`);
17
+ }
18
+ for (const segment of name.split("/")) {
19
+ if (segment === "." || segment === "..") {
20
+ throw new Error(`Invalid worktree name "${name}": must not contain "." or ".." path segments`);
21
+ }
22
+ if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
23
+ throw new Error(`Invalid worktree name "${name}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
24
+ }
25
+ }
26
+ }
27
+ /**
28
+ * Generate a random worktree name.
29
+ */
30
+ export function generateWorktreeName() {
31
+ const adjectives = [
32
+ "swift",
33
+ "calm",
34
+ "bold",
35
+ "keen",
36
+ "bright",
37
+ "cool",
38
+ "deep",
39
+ "fair",
40
+ "gentle",
41
+ "grand",
42
+ ];
43
+ const nouns = [
44
+ "fox",
45
+ "owl",
46
+ "hawk",
47
+ "wolf",
48
+ "bear",
49
+ "lynx",
50
+ "pike",
51
+ "kite",
52
+ "dove",
53
+ "stag",
54
+ ];
55
+ const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
56
+ const noun = nouns[Math.floor(Math.random() * nouns.length)];
57
+ const num = Math.floor(Math.random() * 900) + 100;
58
+ return `${adj}-${noun}-${num}`;
59
+ }
60
+ /**
61
+ * Get the current HEAD commit SHA.
62
+ */
63
+ export function getHeadCommit(cwd) {
64
+ return execSync(`git -C "${cwd}" rev-parse HEAD`, {
65
+ encoding: "utf8",
66
+ stdio: ["ignore", "pipe", "ignore"],
67
+ }).trim();
68
+ }
69
+ /**
70
+ * Create a git worktree for use during a session.
71
+ */
72
+ export function createWorktree(name, cwd) {
73
+ const repoRoot = getGitMainRepoRoot(cwd);
74
+ if (!repoRoot) {
75
+ throw new Error("Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.");
76
+ }
77
+ // Capture HEAD commit before creating worktree (for dirty-check on exit)
78
+ const originalHeadCommit = getHeadCommit(cwd);
79
+ const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
80
+ const branchName = `worktree-${name}`;
81
+ const baseBranch = getDefaultRemoteBranch(cwd);
82
+ // Ensure parent directory exists
83
+ const parentDir = path.dirname(worktreePath);
84
+ if (!fs.existsSync(parentDir)) {
85
+ fs.mkdirSync(parentDir, { recursive: true });
86
+ }
87
+ // Check if worktree already exists
88
+ if (fs.existsSync(worktreePath)) {
89
+ return {
90
+ name,
91
+ path: worktreePath,
92
+ branch: branchName,
93
+ repoRoot,
94
+ isNew: false,
95
+ originalHeadCommit,
96
+ };
97
+ }
98
+ try {
99
+ // Create worktree and branch
100
+ execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
101
+ cwd: repoRoot,
102
+ stdio: ["ignore", "pipe", "pipe"],
103
+ env: {
104
+ ...process.env,
105
+ GIT_TERMINAL_PROMPT: "0",
106
+ GIT_ASKPASS: "",
107
+ },
108
+ });
109
+ return {
110
+ name,
111
+ path: worktreePath,
112
+ branch: branchName,
113
+ repoRoot,
114
+ isNew: true,
115
+ originalHeadCommit,
116
+ };
117
+ }
118
+ catch (error) {
119
+ const stderr = error.stderr?.toString() || "";
120
+ if (stderr.includes("already exists")) {
121
+ // Branch exists but worktree doesn't — attach to existing branch
122
+ try {
123
+ execSync(`git worktree add "${worktreePath}" ${branchName}`, {
124
+ cwd: repoRoot,
125
+ stdio: ["ignore", "pipe", "pipe"],
126
+ env: {
127
+ ...process.env,
128
+ GIT_TERMINAL_PROMPT: "0",
129
+ GIT_ASKPASS: "",
130
+ },
131
+ });
132
+ return {
133
+ name,
134
+ path: worktreePath,
135
+ branch: branchName,
136
+ repoRoot,
137
+ isNew: true,
138
+ originalHeadCommit,
139
+ };
140
+ }
141
+ catch (innerError) {
142
+ throw new Error(`Failed to add worktree: ${innerError.message}`);
143
+ }
144
+ }
145
+ throw new Error(`Failed to create worktree: ${error.message}\n${stderr}`);
146
+ }
147
+ }
148
+ /**
149
+ * Remove a git worktree and its branch.
150
+ */
151
+ export function removeWorktree(info) {
152
+ const repoRoot = info.repoRoot;
153
+ try {
154
+ // Get current branch in worktree before removing
155
+ let currentBranch;
156
+ try {
157
+ currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
158
+ cwd: info.path,
159
+ encoding: "utf8",
160
+ stdio: ["ignore", "pipe", "ignore"],
161
+ }).trim();
162
+ }
163
+ catch {
164
+ // Ignore errors
165
+ }
166
+ // Remove worktree
167
+ execSync(`git worktree remove --force "${info.path}"`, {
168
+ cwd: repoRoot,
169
+ stdio: ["ignore", "pipe", "pipe"],
170
+ });
171
+ // Delete worktree branch
172
+ try {
173
+ execSync(`git branch -D ${info.branch}`, {
174
+ cwd: repoRoot,
175
+ stdio: ["ignore", "pipe", "pipe"],
176
+ });
177
+ }
178
+ catch {
179
+ // Ignore errors
180
+ }
181
+ // Delete current branch if different and not protected
182
+ if (currentBranch &&
183
+ currentBranch !== info.branch &&
184
+ currentBranch !== "HEAD") {
185
+ const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
186
+ const defaultBranchName = defaultRemoteBranch.split("/").pop();
187
+ if (currentBranch !== defaultBranchName &&
188
+ currentBranch !== "main" &&
189
+ currentBranch !== "master") {
190
+ try {
191
+ execSync(`git branch -D ${currentBranch}`, {
192
+ cwd: repoRoot,
193
+ stdio: ["ignore", "pipe", "pipe"],
194
+ });
195
+ }
196
+ catch {
197
+ // Ignore errors
198
+ }
199
+ }
200
+ }
201
+ }
202
+ catch (error) {
203
+ logger.error("Failed to remove worktree or branch:", {
204
+ error: error instanceof Error ? error.message : String(error),
205
+ worktreePath: info.path,
206
+ });
207
+ throw error;
208
+ }
209
+ }
210
+ /**
211
+ * Count uncommitted files and new commits in a worktree.
212
+ * Returns null if git commands fail (fail-closed).
213
+ */
214
+ export function countWorktreeChanges(worktreePath, originalHeadCommit) {
215
+ try {
216
+ const statusOutput = execSync(`git -C "${worktreePath}" status --porcelain`, {
217
+ encoding: "utf8",
218
+ stdio: ["ignore", "pipe", "ignore"],
219
+ });
220
+ const changedFiles = statusOutput
221
+ .split("\n")
222
+ .filter((l) => l.trim() !== "").length;
223
+ if (!originalHeadCommit) {
224
+ return null;
225
+ }
226
+ const revListOutput = execSync(`git -C "${worktreePath}" rev-list --count ${originalHeadCommit}..HEAD`, {
227
+ encoding: "utf8",
228
+ stdio: ["ignore", "pipe", "ignore"],
229
+ });
230
+ const commits = parseInt(revListOutput.trim(), 10) || 0;
231
+ return { changedFiles, commits };
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -83,10 +83,6 @@ export class Agent {
83
83
  // Configuration options storage for dynamic resolution
84
84
  private options: AgentOptions;
85
85
 
86
- // Memory content storage
87
- private _projectMemoryContent: string = "";
88
- private _userMemoryContent: string = "";
89
-
90
86
  // Dynamic configuration getter methods
91
87
  public getGatewayConfig(): GatewayConfig {
92
88
  return this.configurationService.resolveGatewayConfig();
@@ -282,12 +278,20 @@ export class Agent {
282
278
 
283
279
  /** Get project memory content */
284
280
  public get projectMemory(): string {
285
- return this._projectMemoryContent;
281
+ const memoryService =
282
+ this.container.get<import("./services/memory.js").MemoryService>(
283
+ "MemoryService",
284
+ );
285
+ return memoryService?.cachedProjectMemory ?? "";
286
286
  }
287
287
 
288
288
  /** Get user memory content */
289
289
  public get userMemory(): string {
290
- return this._userMemoryContent;
290
+ const memoryService =
291
+ this.container.get<import("./services/memory.js").MemoryService>(
292
+ "MemoryService",
293
+ );
294
+ return memoryService?.cachedUserMemory ?? "";
291
295
  }
292
296
 
293
297
  /** Get combined memory content (project + user + modular rules) */
@@ -414,6 +418,7 @@ export class Agent {
414
418
  * @param options.messages - Optional initial messages for testing convenience
415
419
  * @param options.workdir - Working directory (defaults to process.cwd())
416
420
  * @param options.systemPrompt - Optional custom system prompt
421
+ * @param options.mcpServers - Optional MCP server configs to connect at startup
417
422
  * @returns Promise that resolves to initialized Agent instance
418
423
  *
419
424
  * @example
@@ -479,12 +484,6 @@ export class Agent {
479
484
  memoryRuleManager: this.memoryRuleManager,
480
485
  liveConfigManager: this.liveConfigManager,
481
486
  taskManager: this.taskManager,
482
- setProjectMemory: (content) => {
483
- this._projectMemoryContent = content;
484
- },
485
- setUserMemory: (content) => {
486
- this._userMemoryContent = content;
487
- },
488
487
  resolveAndValidateConfig: () => this.resolveAndValidateConfig(),
489
488
  },
490
489
  options,
@@ -603,6 +602,44 @@ export class Agent {
603
602
  this.options.callbacks?.onBackgroundCurrentTask?.();
604
603
  }
605
604
 
605
+ /**
606
+ * Trigger WorktreeRemove hook before agent destruction.
607
+ * Called from CLI exit dialog when user chooses to remove the worktree.
608
+ * Non-blocking: errors logged but don't prevent removal.
609
+ */
610
+ public async triggerWorktreeRemoveHook(worktreePath: string): Promise<void> {
611
+ if (!this.hookManager.hasHooks("WorktreeRemove")) {
612
+ return;
613
+ }
614
+ try {
615
+ const sessionId = this.messageManager.getSessionId();
616
+ const transcriptPath = this.messageManager.getTranscriptPath();
617
+ const hookResults = await this.hookManager.executeHooks(
618
+ "WorktreeRemove",
619
+ {
620
+ event: "WorktreeRemove",
621
+ projectDir: this.workdir,
622
+ timestamp: new Date(),
623
+ sessionId,
624
+ transcriptPath,
625
+ cwd: this.workdir,
626
+ worktreePath,
627
+ env: Object.fromEntries(
628
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
629
+ ) as Record<string, string>,
630
+ },
631
+ );
632
+ // Process results via messageManager (may not be visible during shutdown)
633
+ this.hookManager.processHookResults(
634
+ "WorktreeRemove",
635
+ hookResults,
636
+ this.messageManager,
637
+ );
638
+ } catch (error) {
639
+ this.logger?.warn("WorktreeRemove hooks execution failed:", error);
640
+ }
641
+ }
642
+
606
643
  /** Destroy managers, clean up resources */
607
644
  public async destroy(): Promise<void> {
608
645
  // Clear notification callback first to prevent any late triggers from
@@ -894,4 +931,16 @@ export class Agent {
894
931
  public get taskListId(): string {
895
932
  return this.taskManager.getTaskListId();
896
933
  }
934
+
935
+ /**
936
+ * Check if there are any running background tasks or active subagents
937
+ */
938
+ public get hasRunningBackgroundWork(): boolean {
939
+ const runningTasks = this.backgroundTaskManager
940
+ .getAllTasks()
941
+ .some((t) => t.status === "running");
942
+ const activeSubagents =
943
+ this.subagentManager.getActiveInstances().length > 0;
944
+ return runningTasks || activeSubagents;
945
+ }
897
946
  }
@@ -19,3 +19,6 @@ export const CRON_CREATE_TOOL_NAME = "CronCreate";
19
19
  export const CRON_DELETE_TOOL_NAME = "CronDelete";
20
20
  export const CRON_LIST_TOOL_NAME = "CronList";
21
21
  export const WEB_FETCH_TOOL_NAME = "WebFetch";
22
+ export const ENTER_WORKTREE_TOOL_NAME = "EnterWorktree";
23
+ export const EXIT_WORKTREE_TOOL_NAME = "ExitWorktree";
24
+ export const TOOL_SEARCH_TOOL_NAME = "ToolSearch";
package/src/index.ts CHANGED
@@ -26,4 +26,5 @@ export * from "./utils/hookMatcher.js";
26
26
  export * from "./utils/tokenCalculation.js";
27
27
  export * from "./utils/gitUtils.js";
28
28
  export * from "./utils/nameGenerator.js";
29
+ export * from "./utils/worktreeSession.js";
29
30
  export * from "./types/index.js";
@@ -53,20 +53,20 @@ export class AIManager {
53
53
  private abortController: AbortController | null = null;
54
54
  onLoadingChange?: (loading: boolean) => void;
55
55
  private toolAbortController: AbortController | null = null;
56
- private workdir: string;
57
56
  private systemPrompt?: string;
58
57
  private subagentType?: string; // Store subagent type for hook context
59
58
  private stream: boolean; // Streaming mode flag
60
59
  private modelOverride?: string;
61
60
  private consecutiveCompactionFailures: number = 0;
62
61
  private readonly maxTurns?: number;
62
+ /** Tracks which deferred tools have been discovered via ToolSearch */
63
+ private discoveredTools = new Set<string>();
63
64
 
64
65
  // Service overrides
65
66
  constructor(
66
67
  private container: Container,
67
68
  options: AIManagerOptions,
68
69
  ) {
69
- this.workdir = options.workdir;
70
70
  this.systemPrompt = options.systemPrompt;
71
71
  this.subagentType = options.subagentType; // Store subagent type
72
72
  this.stream = options.stream ?? true; // Default to true if not specified
@@ -166,7 +166,16 @@ export class AIManager {
166
166
  }
167
167
 
168
168
  public getWorkdir(): string {
169
- return this.workdir;
169
+ return this.container.get<string>("Workdir") ?? process.cwd();
170
+ }
171
+
172
+ /**
173
+ * Update the working directory mid-session (e.g., when entering/exiting a worktree).
174
+ * Also updates process.chdir() so bash commands use the new directory.
175
+ */
176
+ public setWorkdir(newWorkdir: string): void {
177
+ this.container.register("Workdir", newWorkdir);
178
+ process.chdir(newWorkdir);
170
179
  }
171
180
 
172
181
  private isCompacting: boolean = false;
@@ -185,8 +194,9 @@ export class AIManager {
185
194
  return this.toolManager.getToolsConfig({
186
195
  availableSubagents,
187
196
  availableSkills,
188
- workdir: this.workdir,
197
+ workdir: this.getWorkdir(),
189
198
  isSubagent: !!this.subagentType,
199
+ discoveredTools: this.discoveredTools,
190
200
  });
191
201
  }
192
202
 
@@ -233,7 +243,7 @@ export class AIManager {
233
243
  .find((plugin) => plugin.name === toolName);
234
244
  if (toolPlugin?.formatCompactParams) {
235
245
  const context: ToolContext = {
236
- workdir: this.workdir,
246
+ workdir: this.getWorkdir(),
237
247
  taskManager: this.taskManager,
238
248
  };
239
249
  return toolPlugin.formatCompactParams(toolArgs, context);
@@ -332,7 +342,7 @@ export class AIManager {
332
342
 
333
343
  // 2. Working directory
334
344
  contextParts.push(
335
- `\n\n[Working Directory]\nCurrent working directory: ${this.workdir}`,
345
+ `\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`,
336
346
  );
337
347
 
338
348
  // 3. Plan mode context
@@ -620,10 +630,10 @@ export class AIManager {
620
630
 
621
631
  if (this.getAutoMemoryEnabled()) {
622
632
  const directory = this.memoryService.getAutoMemoryDirectory(
623
- this.workdir,
633
+ this.getWorkdir(),
624
634
  );
625
635
  const content = await this.memoryService.getAutoMemoryContent(
626
- this.workdir,
636
+ this.getWorkdir(),
627
637
  );
628
638
  autoMemoryOptions = { directory, content };
629
639
  }
@@ -635,14 +645,14 @@ export class AIManager {
635
645
  messages: recentMessages,
636
646
  sessionId: this.messageManager.getSessionId(),
637
647
  abortSignal: abortController.signal,
638
- workdir: this.workdir, // Pass working directory
648
+ workdir: this.getWorkdir(), // Pass working directory
639
649
  tools: toolsConfig, // Pass filtered tool configuration
640
650
  model: model, // Use passed model
641
651
  systemPrompt: buildSystemPrompt(
642
652
  this.systemPrompt,
643
653
  filteredToolPlugins,
644
654
  {
645
- workdir: this.workdir,
655
+ workdir: this.getWorkdir(),
646
656
  memory: combinedMemory,
647
657
  language: this.getLanguage(),
648
658
  isSubagent: !!this.subagentType,
@@ -892,7 +902,7 @@ export class AIManager {
892
902
  const context: ToolContext = {
893
903
  abortSignal: toolAbortController.signal,
894
904
  backgroundTaskManager: this.backgroundTaskManager,
895
- workdir: this.workdir,
905
+ workdir: this.getWorkdir(),
896
906
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
897
907
  sessionId: this.messageManager.getSessionId(),
898
908
  toolCallId: toolId,
@@ -952,6 +962,11 @@ export class AIManager {
952
962
  toolArgs,
953
963
  toolResult,
954
964
  );
965
+
966
+ // Track discovered tools from ToolSearch results
967
+ if (toolName === "ToolSearch" && toolResult.success) {
968
+ this.trackDiscoveredTools(toolResult.content);
969
+ }
955
970
  } catch (toolError) {
956
971
  const errorMessage =
957
972
  toolError instanceof Error
@@ -1196,11 +1211,11 @@ export class AIManager {
1196
1211
 
1197
1212
  const context: ExtendedHookExecutionContext = {
1198
1213
  event: hookName,
1199
- projectDir: this.workdir,
1214
+ projectDir: this.getWorkdir(),
1200
1215
  timestamp: new Date(),
1201
1216
  sessionId: this.messageManager.getSessionId(),
1202
1217
  transcriptPath: this.messageManager.getTranscriptPath(),
1203
- cwd: this.workdir,
1218
+ cwd: this.getWorkdir(),
1204
1219
  subagentType: this.subagentType, // Include subagent type in hook context
1205
1220
  // Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
1206
1221
  env: Object.fromEntries(
@@ -1252,7 +1267,7 @@ export class AIManager {
1252
1267
  if (autoMemoryService) {
1253
1268
  // Trigger extraction, but don't block the return.
1254
1269
  // onTurnEnd itself returns quickly after forking.
1255
- autoMemoryService.onTurnEnd(this.workdir).catch((err) => {
1270
+ autoMemoryService.onTurnEnd(this.getWorkdir()).catch((err) => {
1256
1271
  logger?.error("Auto-memory extraction trigger failed:", err);
1257
1272
  });
1258
1273
  }
@@ -1283,12 +1298,12 @@ export class AIManager {
1283
1298
  try {
1284
1299
  const context: ExtendedHookExecutionContext = {
1285
1300
  event: "PreToolUse",
1286
- projectDir: this.workdir,
1301
+ projectDir: this.getWorkdir(),
1287
1302
  timestamp: new Date(),
1288
1303
  toolName,
1289
1304
  sessionId: this.messageManager.getSessionId(),
1290
1305
  transcriptPath: this.messageManager.getTranscriptPath(),
1291
- cwd: this.workdir,
1306
+ cwd: this.getWorkdir(),
1292
1307
  toolInput,
1293
1308
  subagentType: this.subagentType, // Include subagent type in hook context
1294
1309
  env: Object.fromEntries(
@@ -1350,12 +1365,12 @@ export class AIManager {
1350
1365
  try {
1351
1366
  const context: ExtendedHookExecutionContext = {
1352
1367
  event: "PostToolUse",
1353
- projectDir: this.workdir,
1368
+ projectDir: this.getWorkdir(),
1354
1369
  timestamp: new Date(),
1355
1370
  toolName,
1356
1371
  sessionId: this.messageManager.getSessionId(),
1357
1372
  transcriptPath: this.messageManager.getTranscriptPath(),
1358
- cwd: this.workdir,
1373
+ cwd: this.getWorkdir(),
1359
1374
  toolInput,
1360
1375
  toolResponse,
1361
1376
  subagentType: this.subagentType, // Include subagent type in hook context
@@ -1397,4 +1412,44 @@ export class AIManager {
1397
1412
  logger?.error("PostToolUse hook execution failed:", error);
1398
1413
  }
1399
1414
  }
1415
+
1416
+ /**
1417
+ * Parse ToolSearch result content to extract discovered tool names.
1418
+ * ToolSearch returns content like "ToolName: description\nParameters: {...}"
1419
+ * or shortResult like "Discovered tools: ToolA, ToolB".
1420
+ */
1421
+ private trackDiscoveredTools(content: string): void {
1422
+ // Try to extract tool names from shortResult-style content
1423
+ const discoveredMatch = content.match(/Discovered tools?: ([\w-, ]+)/);
1424
+ if (discoveredMatch) {
1425
+ const names = discoveredMatch[1]!
1426
+ .split(",")
1427
+ .map((n) => n.trim())
1428
+ .filter(Boolean);
1429
+ for (const name of names) {
1430
+ this.discoveredTools.add(name);
1431
+ }
1432
+ logger?.debug("Discovered tools:", names);
1433
+ return;
1434
+ }
1435
+
1436
+ // Fallback: extract tool names from "ToolName: description" pattern
1437
+ const lines = content.split("\n");
1438
+ const nonToolKeywords = new Set([
1439
+ "parameters",
1440
+ "description",
1441
+ "result",
1442
+ "error",
1443
+ "content",
1444
+ "type",
1445
+ "properties",
1446
+ "required",
1447
+ ]);
1448
+ for (const line of lines) {
1449
+ const toolMatch = line.match(/^([\w-]+):/);
1450
+ if (toolMatch && !nonToolKeywords.has(toolMatch[1]!.toLowerCase())) {
1451
+ this.discoveredTools.add(toolMatch[1]!);
1452
+ }
1453
+ }
1454
+ }
1400
1455
  }
@@ -377,6 +377,11 @@ export class HookManager {
377
377
  messageManager.addErrorBlock(errorMessage);
378
378
  return { shouldBlock: false };
379
379
 
380
+ case "WorktreeRemove":
381
+ // Non-blocking for cleanup, log error but don't block
382
+ messageManager.addErrorBlock(errorMessage);
383
+ return { shouldBlock: false };
384
+
380
385
  case "SessionStart":
381
386
  // Non-blocking for startup, show error in error block
382
387
  messageManager.addErrorBlock(errorMessage);
@@ -591,6 +596,7 @@ export class HookManager {
591
596
  event === "Stop" ||
592
597
  event === "SubagentStop" ||
593
598
  event === "WorktreeCreate" ||
599
+ event === "WorktreeRemove" ||
594
600
  event === "SessionStart" ||
595
601
  event === "SessionEnd") &&
596
602
  context.toolName !== undefined
@@ -669,6 +675,7 @@ export class HookManager {
669
675
  event === "Stop" ||
670
676
  event === "SubagentStop" ||
671
677
  event === "WorktreeCreate" ||
678
+ event === "WorktreeRemove" ||
672
679
  event === "SessionStart" ||
673
680
  event === "SessionEnd"
674
681
  ) {
@@ -731,6 +738,7 @@ export class HookManager {
731
738
  event === "Stop" ||
732
739
  event === "SubagentStop" ||
733
740
  event === "WorktreeCreate" ||
741
+ event === "WorktreeRemove" ||
734
742
  event === "SessionStart" ||
735
743
  event === "SessionEnd") &&
736
744
  config.matcher
@@ -772,6 +780,7 @@ export class HookManager {
772
780
  SubagentStop: 0,
773
781
  PermissionRequest: 0,
774
782
  WorktreeCreate: 0,
783
+ WorktreeRemove: 0,
775
784
  SessionStart: 0,
776
785
  SessionEnd: 0,
777
786
  },
@@ -786,6 +795,7 @@ export class HookManager {
786
795
  SubagentStop: 0,
787
796
  PermissionRequest: 0,
788
797
  WorktreeCreate: 0,
798
+ WorktreeRemove: 0,
789
799
  SessionStart: 0,
790
800
  SessionEnd: 0,
791
801
  };