wave-agent-sdk 0.13.4 → 0.13.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 (91) hide show
  1. package/builtin/skills/settings/HOOKS.md +25 -0
  2. package/builtin/skills/settings/MCP.md +22 -0
  3. package/builtin/skills/settings/SKILL.md +4 -1
  4. package/dist/agent.d.ts +21 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +102 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/managers/aiManager.d.ts +5 -0
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +19 -4
  13. package/dist/managers/bangManager.d.ts +1 -0
  14. package/dist/managers/bangManager.d.ts.map +1 -1
  15. package/dist/managers/bangManager.js +1 -0
  16. package/dist/managers/hookManager.d.ts +5 -1
  17. package/dist/managers/hookManager.d.ts.map +1 -1
  18. package/dist/managers/hookManager.js +55 -5
  19. package/dist/managers/lspManager.d.ts.map +1 -1
  20. package/dist/managers/lspManager.js +17 -2
  21. package/dist/managers/mcpManager.d.ts.map +1 -1
  22. package/dist/managers/mcpManager.js +20 -6
  23. package/dist/managers/messageManager.d.ts.map +1 -1
  24. package/dist/managers/messageManager.js +22 -0
  25. package/dist/managers/messageQueue.d.ts +20 -0
  26. package/dist/managers/messageQueue.d.ts.map +1 -0
  27. package/dist/managers/messageQueue.js +29 -0
  28. package/dist/managers/permissionManager.d.ts +5 -7
  29. package/dist/managers/permissionManager.d.ts.map +1 -1
  30. package/dist/managers/permissionManager.js +27 -22
  31. package/dist/managers/pluginManager.d.ts.map +1 -1
  32. package/dist/managers/pluginManager.js +5 -3
  33. package/dist/managers/skillManager.d.ts.map +1 -1
  34. package/dist/managers/skillManager.js +5 -0
  35. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  36. package/dist/managers/slashCommandManager.js +12 -1
  37. package/dist/managers/subagentManager.d.ts.map +1 -1
  38. package/dist/managers/subagentManager.js +6 -18
  39. package/dist/services/autoMemoryService.d.ts.map +1 -1
  40. package/dist/services/autoMemoryService.js +1 -0
  41. package/dist/services/hook.d.ts +4 -0
  42. package/dist/services/hook.d.ts.map +1 -1
  43. package/dist/services/hook.js +10 -0
  44. package/dist/services/interactionService.d.ts.map +1 -1
  45. package/dist/services/interactionService.js +3 -0
  46. package/dist/services/pluginLoader.d.ts.map +1 -1
  47. package/dist/services/pluginLoader.js +3 -1
  48. package/dist/tools/bashTool.d.ts.map +1 -1
  49. package/dist/tools/bashTool.js +33 -2
  50. package/dist/tools/types.d.ts +2 -0
  51. package/dist/tools/types.d.ts.map +1 -1
  52. package/dist/types/agent.d.ts +4 -0
  53. package/dist/types/agent.d.ts.map +1 -1
  54. package/dist/types/hooks.d.ts +6 -1
  55. package/dist/types/hooks.d.ts.map +1 -1
  56. package/dist/types/hooks.js +4 -0
  57. package/dist/types/lsp.d.ts +2 -0
  58. package/dist/types/lsp.d.ts.map +1 -1
  59. package/dist/types/mcp.d.ts +2 -0
  60. package/dist/types/mcp.d.ts.map +1 -1
  61. package/dist/types/skills.d.ts +1 -0
  62. package/dist/types/skills.d.ts.map +1 -1
  63. package/dist/utils/containerSetup.d.ts.map +1 -1
  64. package/dist/utils/containerSetup.js +4 -1
  65. package/package.json +1 -1
  66. package/src/agent.ts +122 -2
  67. package/src/index.ts +1 -0
  68. package/src/managers/aiManager.ts +35 -5
  69. package/src/managers/bangManager.ts +2 -0
  70. package/src/managers/hookManager.ts +78 -19
  71. package/src/managers/lspManager.ts +23 -2
  72. package/src/managers/mcpManager.ts +29 -6
  73. package/src/managers/messageManager.ts +38 -0
  74. package/src/managers/messageQueue.ts +41 -0
  75. package/src/managers/permissionManager.ts +32 -26
  76. package/src/managers/pluginManager.ts +5 -3
  77. package/src/managers/skillManager.ts +9 -0
  78. package/src/managers/slashCommandManager.ts +16 -4
  79. package/src/managers/subagentManager.ts +10 -25
  80. package/src/services/autoMemoryService.ts +1 -0
  81. package/src/services/hook.ts +15 -0
  82. package/src/services/interactionService.ts +3 -0
  83. package/src/services/pluginLoader.ts +3 -1
  84. package/src/tools/bashTool.ts +39 -2
  85. package/src/tools/types.ts +2 -0
  86. package/src/types/agent.ts +4 -0
  87. package/src/types/hooks.ts +13 -2
  88. package/src/types/lsp.ts +2 -0
  89. package/src/types/mcp.ts +2 -0
  90. package/src/types/skills.ts +1 -0
  91. package/src/utils/containerSetup.ts +5 -1
@@ -166,28 +166,42 @@ export class HookManager {
166
166
  ? { timeout: hookCommand.timeout * 1000 }
167
167
  : undefined;
168
168
 
169
+ // Build execution context with WAVE_PLUGIN_ROOT if this is a plugin hook
170
+ let command = hookCommand.command;
171
+ const execContext: typeof context = hookCommand.pluginRoot
172
+ ? {
173
+ ...context,
174
+ env: {
175
+ ...("env" in context ? (context.env ?? {}) : {}),
176
+ WAVE_PLUGIN_ROOT: hookCommand.pluginRoot,
177
+ },
178
+ }
179
+ : context;
180
+
181
+ // Substitute ${WAVE_PLUGIN_ROOT} in the command string (same pattern as Claude Code)
182
+ if (hookCommand.pluginRoot) {
183
+ command = command.replace(
184
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
185
+ hookCommand.pluginRoot,
186
+ );
187
+ }
188
+
169
189
  if (hookCommand.async) {
170
190
  // Execute async command without awaiting
171
- executeCommand(hookCommand.command, context, options).catch(
172
- (error) => {
173
- const errorMessage =
174
- error instanceof Error
175
- ? error.message
176
- : "Unknown execution error";
177
- logger?.error(
178
- `[HookManager] Async hook command ${commandIndex + 1} failed: ${errorMessage}`,
179
- );
180
- },
181
- );
191
+ executeCommand(command, execContext, options).catch((error) => {
192
+ const errorMessage =
193
+ error instanceof Error
194
+ ? error.message
195
+ : "Unknown execution error";
196
+ logger?.error(
197
+ `[HookManager] Async hook command ${commandIndex + 1} failed: ${errorMessage}`,
198
+ );
199
+ });
182
200
  // Async hooks are not included in results to prevent blocking
183
201
  continue;
184
202
  }
185
203
 
186
- const result = await executeCommand(
187
- hookCommand.command,
188
- context,
189
- options,
190
- );
204
+ const result = await executeCommand(command, execContext, options);
191
205
  results.push(result);
192
206
 
193
207
  // Continue with next command even if this one fails
@@ -639,7 +653,8 @@ export class HookManager {
639
653
  event === "UserPromptSubmit" ||
640
654
  event === "Stop" ||
641
655
  event === "SubagentStop" ||
642
- event === "WorktreeCreate"
656
+ event === "WorktreeCreate" ||
657
+ event === "CwdChanged"
643
658
  ) {
644
659
  return true;
645
660
  }
@@ -739,6 +754,7 @@ export class HookManager {
739
754
  SubagentStop: 0,
740
755
  PermissionRequest: 0,
741
756
  WorktreeCreate: 0,
757
+ CwdChanged: 0,
742
758
  },
743
759
  };
744
760
  }
@@ -751,6 +767,7 @@ export class HookManager {
751
767
  SubagentStop: 0,
752
768
  PermissionRequest: 0,
753
769
  WorktreeCreate: 0,
770
+ CwdChanged: 0,
754
771
  };
755
772
 
756
773
  let totalConfigs = 0;
@@ -775,14 +792,56 @@ export class HookManager {
775
792
  };
776
793
  }
777
794
 
795
+ /**
796
+ * Execute CwdChanged hooks.
797
+ */
798
+ async executeCwdChangedHooks(
799
+ oldCwd: string,
800
+ newCwd: string,
801
+ sessionId: string,
802
+ transcriptPath: string,
803
+ env: Record<string, string>,
804
+ ): Promise<HookExecutionResult[]> {
805
+ const context: ExtendedHookExecutionContext = {
806
+ event: "CwdChanged",
807
+ projectDir: this.workdir,
808
+ timestamp: new Date(),
809
+ sessionId,
810
+ transcriptPath,
811
+ cwd: newCwd,
812
+ oldCwd,
813
+ newCwd,
814
+ env,
815
+ };
816
+ const results = await this.executeHooks("CwdChanged", context);
817
+ if (results.length > 0) {
818
+ // For CwdChanged hooks, we don't block, just log errors
819
+ this.processHookResults("CwdChanged", results);
820
+ }
821
+ return results;
822
+ }
823
+
778
824
  /**
779
825
  * Register hooks provided by a plugin
780
826
  */
781
- registerPluginHooks(hooks: PartialHookConfiguration): void {
827
+ registerPluginHooks(
828
+ pluginRoot: string,
829
+ hooks: PartialHookConfiguration,
830
+ ): void {
782
831
  if (!this.configuration) {
783
832
  this.configuration = {};
784
833
  }
785
834
 
786
- this.mergeHooksConfiguration(this.configuration, hooks);
835
+ // Stamp pluginRoot on each hook command
836
+ const stampedHooks: PartialHookConfiguration = {};
837
+ for (const [event, configs] of Object.entries(hooks)) {
838
+ if (!isValidHookEvent(event)) continue;
839
+ stampedHooks[event] = configs.map((config) => ({
840
+ ...config,
841
+ hooks: config.hooks.map((cmd) => ({ ...cmd, pluginRoot })),
842
+ }));
843
+ }
844
+
845
+ this.mergeHooksConfiguration(this.configuration, stampedHooks);
787
846
  }
788
847
  }
@@ -90,9 +90,30 @@ export class LspManager implements ILspManager {
90
90
  );
91
91
 
92
92
  try {
93
- const proc = spawn(config.command, config.args || [], {
93
+ const env = { ...process.env, ...config.env };
94
+
95
+ // For plugin servers, substitute ${WAVE_PLUGIN_ROOT} in command/args/env
96
+ let command = config.command;
97
+ let args = config.args || [];
98
+ if (config.pluginRoot) {
99
+ env.WAVE_PLUGIN_ROOT = config.pluginRoot;
100
+ command = command.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, config.pluginRoot);
101
+ args = args.map((arg) =>
102
+ arg.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, config.pluginRoot!),
103
+ );
104
+ // Also expand WAVE_PLUGIN_ROOT in user-provided env values
105
+ if (config.env) {
106
+ for (const [key, value] of Object.entries(config.env)) {
107
+ env[key] = value.replace(
108
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
109
+ config.pluginRoot!,
110
+ );
111
+ }
112
+ }
113
+ }
114
+ const proc = spawn(command, args, {
94
115
  cwd: config.workspaceFolder || this.workdir,
95
- env: { ...process.env, ...config.env },
116
+ env,
96
117
  stdio: ["pipe", "pipe", "pipe"],
97
118
  });
98
119
 
@@ -348,13 +348,36 @@ export class McpManager {
348
348
  logger?.info(`Connected to MCP server ${name} using SSE (fallback)`);
349
349
  }
350
350
  } else if (server.config.command) {
351
+ const env: Record<string, string> = {
352
+ ...(process.env as Record<string, string>),
353
+ ...(server.config.env || {}),
354
+ };
355
+
356
+ // For plugin servers, substitute ${WAVE_PLUGIN_ROOT} in command/args/env
357
+ // (same pattern as Claude Code's substitutePluginVariables)
358
+ let command = server.config.command;
359
+ let args = server.config.args || [];
360
+ if (server.config.pluginRoot) {
361
+ env.WAVE_PLUGIN_ROOT = server.config.pluginRoot;
362
+ command = command.replace(
363
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
364
+ server.config.pluginRoot,
365
+ );
366
+ args = args.map((arg) =>
367
+ arg.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, server.config.pluginRoot!),
368
+ );
369
+ // Also expand WAVE_PLUGIN_ROOT in user-provided env values
370
+ for (const [key, value] of Object.entries(server.config.env || {})) {
371
+ env[key] = value.replace(
372
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
373
+ server.config.pluginRoot!,
374
+ );
375
+ }
376
+ }
351
377
  transport = new StdioClientTransport({
352
- command: server.config.command,
353
- args: server.config.args || [],
354
- env: {
355
- ...(process.env as Record<string, string>),
356
- ...(server.config.env || {}),
357
- },
378
+ command,
379
+ args,
380
+ env,
358
381
  cwd: this.workdir, // Use the agent's workdir as the process working directory
359
382
  stderr: "pipe", // Pipe stderr to capture it
360
383
  });
@@ -654,6 +654,25 @@ export class MessageManager {
654
654
  const lastMessage = this.messages[this.messages.length - 1];
655
655
  if (lastMessage.role !== "assistant") return;
656
656
 
657
+ // Finalize any streaming reasoning blocks before text content arrives
658
+ const reasoningIndex = lastMessage.blocks.findIndex(
659
+ (block) =>
660
+ block.type === "reasoning" &&
661
+ (block as { stage?: string }).stage === "streaming",
662
+ );
663
+ if (reasoningIndex >= 0) {
664
+ const reasoningBlock = lastMessage.blocks[reasoningIndex] as {
665
+ type: "reasoning";
666
+ content: string;
667
+ stage?: string;
668
+ };
669
+ lastMessage.blocks[reasoningIndex] = {
670
+ type: "reasoning" as const,
671
+ content: reasoningBlock.content,
672
+ stage: "end" as const,
673
+ };
674
+ }
675
+
657
676
  // Get the current content to calculate the chunk
658
677
  const textBlockIndex = lastMessage.blocks.findIndex(
659
678
  (block) => block.type === "text",
@@ -705,6 +724,25 @@ export class MessageManager {
705
724
  const lastMessage = this.messages[this.messages.length - 1];
706
725
  if (lastMessage.role !== "assistant") return;
707
726
 
727
+ // Finalize any streaming text blocks before reasoning content arrives
728
+ const textIndex = lastMessage.blocks.findIndex(
729
+ (block) =>
730
+ block.type === "text" &&
731
+ (block as { stage?: string }).stage === "streaming",
732
+ );
733
+ if (textIndex >= 0) {
734
+ const textBlock = lastMessage.blocks[textIndex] as {
735
+ type: "text";
736
+ content: string;
737
+ stage?: string;
738
+ };
739
+ lastMessage.blocks[textIndex] = {
740
+ type: "text" as const,
741
+ content: textBlock.content,
742
+ stage: "end" as const,
743
+ };
744
+ }
745
+
708
746
  // Get the current reasoning content to calculate the chunk
709
747
  const reasoningBlockIndex = lastMessage.blocks.findIndex(
710
748
  (block) => block.type === "reasoning",
@@ -0,0 +1,41 @@
1
+ export interface QueuedMessage {
2
+ type?: "message" | "bang";
3
+ content: string;
4
+ images?: Array<{ path: string; mimeType: string }>;
5
+ longTextMap?: Record<string, string>;
6
+ }
7
+
8
+ export class MessageQueue {
9
+ private queue: QueuedMessage[] = [];
10
+ onMessageEnqueued?: () => void;
11
+
12
+ enqueue(message: QueuedMessage): void {
13
+ this.queue.push(message);
14
+ this.onMessageEnqueued?.();
15
+ }
16
+
17
+ dequeue(): QueuedMessage | null {
18
+ return this.queue.shift() ?? null;
19
+ }
20
+
21
+ clear(): void {
22
+ this.queue = [];
23
+ }
24
+
25
+ hasPending(): boolean {
26
+ return this.queue.length > 0;
27
+ }
28
+
29
+ getQueue(): QueuedMessage[] {
30
+ return [...this.queue];
31
+ }
32
+
33
+ removeAt(index: number): boolean {
34
+ if (index < 0 || index >= this.queue.length) {
35
+ return false;
36
+ }
37
+ this.queue.splice(index, 1);
38
+ this.onMessageEnqueued?.();
39
+ return true;
40
+ }
41
+ }
@@ -113,8 +113,6 @@ export interface PermissionManagerOptions {
113
113
  additionalDirectories?: string[];
114
114
  /** System additional directories (persistent across reloads) */
115
115
  systemAdditionalDirectories?: string[];
116
- /** The main working directory */
117
- workdir?: string;
118
116
  /** Path to the current plan file */
119
117
  planFilePath?: string;
120
118
  /** Optional logger */
@@ -130,10 +128,10 @@ export class PermissionManager {
130
128
  private temporaryRules: string[] = [];
131
129
  private additionalDirectories: string[] = [];
132
130
  private systemAdditionalDirectories: string[] = [];
133
- private workdir?: string;
134
131
  private planFilePath?: string;
135
132
  private worktreeName?: string;
136
133
  private mainRepoRoot?: string;
134
+ private originalWorkdir?: string;
137
135
  private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
138
136
  private _logger?: Logger;
139
137
 
@@ -146,7 +144,6 @@ export class PermissionManager {
146
144
  this.deniedRules = options.deniedRules || [];
147
145
  this.instanceAllowedRules = options.instanceAllowedRules || [];
148
146
  this.instanceDeniedRules = options.instanceDeniedRules || [];
149
- this.workdir = options.workdir;
150
147
  this.planFilePath = options.planFilePath;
151
148
  this._logger = options.logger;
152
149
  this.updateAdditionalDirectories(options.additionalDirectories || []);
@@ -156,6 +153,14 @@ export class PermissionManager {
156
153
 
157
154
  this.worktreeName = this.container.get<string>("WorktreeName");
158
155
  this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
156
+ this.originalWorkdir = this.container.get<string>("Workdir");
157
+ }
158
+
159
+ /**
160
+ * Resolve the working directory from the DI container
161
+ */
162
+ private getWorkdir(): string | undefined {
163
+ return this.container.get<string>("Workdir");
159
164
  }
160
165
 
161
166
  /**
@@ -272,9 +277,10 @@ export class PermissionManager {
272
277
  * Update the additional directories (e.g., when configuration reloads)
273
278
  */
274
279
  updateAdditionalDirectories(directories: string[]): void {
280
+ const workdir = this.originalWorkdir;
275
281
  this.additionalDirectories = directories.map((dir) => {
276
- if (this.workdir && !path.isAbsolute(dir)) {
277
- return path.resolve(this.workdir, dir);
282
+ if (workdir && !path.isAbsolute(dir)) {
283
+ return path.resolve(workdir, dir);
278
284
  }
279
285
  return path.resolve(dir);
280
286
  });
@@ -284,9 +290,10 @@ export class PermissionManager {
284
290
  * Add a system-level additional directory that is persistent across configuration reloads
285
291
  */
286
292
  public addSystemAdditionalDirectory(directory: string): void {
293
+ const workdir = this.originalWorkdir;
287
294
  const resolvedPath =
288
- this.workdir && !path.isAbsolute(directory)
289
- ? path.resolve(this.workdir, directory)
295
+ workdir && !path.isAbsolute(directory)
296
+ ? path.resolve(workdir, directory)
290
297
  : path.resolve(directory);
291
298
 
292
299
  if (!this.systemAdditionalDirectories.includes(resolvedPath)) {
@@ -294,13 +301,6 @@ export class PermissionManager {
294
301
  }
295
302
  }
296
303
 
297
- /**
298
- * Update the working directory
299
- */
300
- updateWorkdir(workdir: string): void {
301
- this.workdir = workdir;
302
- }
303
-
304
304
  /**
305
305
  * Set the current plan file path
306
306
  */
@@ -322,7 +322,7 @@ export class PermissionManager {
322
322
  targetPath: string,
323
323
  workdir?: string,
324
324
  ): { isInside: boolean; resolvedPath: string } {
325
- const effectiveWorkdir = workdir || this.workdir;
325
+ const effectiveWorkdir = this.originalWorkdir || workdir;
326
326
 
327
327
  // Resolve the target path relative to effectiveWorkdir if it's not absolute
328
328
  const absolutePath =
@@ -432,21 +432,25 @@ export class PermissionManager {
432
432
  }
433
433
 
434
434
  // 0. Check worktree safety for Write and Edit tools
435
+ const currentWorkdir = this.getWorkdir();
435
436
  if (
436
437
  this.worktreeName &&
437
438
  this.mainRepoRoot &&
438
- this.workdir &&
439
+ currentWorkdir &&
439
440
  (context.toolName === WRITE_TOOL_NAME ||
440
441
  context.toolName === EDIT_TOOL_NAME)
441
442
  ) {
442
443
  const targetPath = context.toolInput?.file_path as string | undefined;
443
444
  if (targetPath) {
444
- const absoluteTargetPath = path.resolve(this.workdir, targetPath);
445
+ const absoluteTargetPath = path.resolve(currentWorkdir, targetPath);
445
446
  const isInsideMainRepo = isPathInside(
446
447
  absoluteTargetPath,
447
448
  this.mainRepoRoot,
448
449
  );
449
- const isInsideWorktree = isPathInside(absoluteTargetPath, this.workdir);
450
+ const isInsideWorktree = isPathInside(
451
+ absoluteTargetPath,
452
+ currentWorkdir,
453
+ );
450
454
 
451
455
  // If it's inside the main repo but NOT inside the current worktree
452
456
  if (isInsideMainRepo && !isInsideWorktree) {
@@ -455,11 +459,11 @@ export class PermissionManager {
455
459
  targetPath,
456
460
  worktreeName: this.worktreeName,
457
461
  mainRepoRoot: this.mainRepoRoot,
458
- workdir: this.workdir,
462
+ workdir: currentWorkdir,
459
463
  });
460
464
  return {
461
465
  behavior: "deny",
462
- message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${this.workdir}`,
466
+ message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${currentWorkdir}`,
463
467
  };
464
468
  }
465
469
  }
@@ -808,12 +812,13 @@ export class PermissionManager {
808
812
  }
809
813
 
810
814
  // If direct match fails, try matching relative path if targetPath is absolute and pattern is relative
815
+ const currentWorkdir = this.getWorkdir();
811
816
  if (
812
817
  path.isAbsolute(targetPath) &&
813
818
  !path.isAbsolute(pattern) &&
814
- this.workdir
819
+ currentWorkdir
815
820
  ) {
816
- const relativePath = path.relative(this.workdir, targetPath);
821
+ const relativePath = path.relative(currentWorkdir, targetPath);
817
822
  // Ensure the path is not outside the workdir (doesn't start with ..)
818
823
  if (
819
824
  !relativePath.startsWith("..") &&
@@ -1056,7 +1061,8 @@ export class PermissionManager {
1056
1061
  * @param rule - The rule to add (e.g., "Bash(ls)")
1057
1062
  */
1058
1063
  public async addPermissionRule(rule: string): Promise<void> {
1059
- if (!this.workdir) {
1064
+ const workdir = this.originalWorkdir;
1065
+ if (!workdir) {
1060
1066
  throw new Error("Working directory not set in PermissionManager");
1061
1067
  }
1062
1068
 
@@ -1065,7 +1071,7 @@ export class PermissionManager {
1065
1071
  const bashMatch = rule.match(/^Bash\((.*)\)$/);
1066
1072
  if (bashMatch) {
1067
1073
  const command = bashMatch[1];
1068
- rulesToAdd = this.expandBashRule(command, this.workdir);
1074
+ rulesToAdd = this.expandBashRule(command, workdir);
1069
1075
  }
1070
1076
 
1071
1077
  const configurationService = this.container.get<ConfigurationService>(
@@ -1085,7 +1091,7 @@ export class PermissionManager {
1085
1091
  // 3. Persist to settings.local.json
1086
1092
  try {
1087
1093
  if (configurationService) {
1088
- await configurationService.addAllowedRule(this.workdir, ruleToAdd);
1094
+ await configurationService.addAllowedRule(workdir, ruleToAdd);
1089
1095
  this._logger?.debug("Persistent permission rule added", {
1090
1096
  rule: ruleToAdd,
1091
1097
  });
@@ -171,7 +171,8 @@ export class PluginManager {
171
171
 
172
172
  if (this.lspManager && plugin.lspConfig) {
173
173
  for (const [language, config] of Object.entries(plugin.lspConfig)) {
174
- this.lspManager.registerServer(language, config);
174
+ const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
175
+ this.lspManager.registerServer(language, configWithPluginRoot);
175
176
  }
176
177
  }
177
178
 
@@ -179,12 +180,13 @@ export class PluginManager {
179
180
  for (const [name, config] of Object.entries(
180
181
  plugin.mcpConfig.mcpServers,
181
182
  )) {
182
- this.mcpManager.addServer(name, config);
183
+ const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
184
+ this.mcpManager.addServer(name, configWithPluginRoot);
183
185
  }
184
186
  }
185
187
 
186
188
  if (this.hookManager && plugin.hooksConfig) {
187
- this.hookManager.registerPluginHooks(plugin.hooksConfig);
189
+ this.hookManager.registerPluginHooks(plugin.path, plugin.hooksConfig);
188
190
  }
189
191
 
190
192
  this.plugins.set(manifest.name, plugin);
@@ -443,6 +443,14 @@ export class SkillManager extends EventEmitter {
443
443
  // 2. Substitute ${WAVE_SKILL_DIR} with the skill's directory path
444
444
  mainContent = mainContent.replace(/\$\{WAVE_SKILL_DIR\}/g, skill.skillPath);
445
445
 
446
+ // 3. Substitute ${WAVE_PLUGIN_ROOT} with the skill's plugin root path
447
+ if (skill.pluginRoot) {
448
+ mainContent = mainContent.replace(
449
+ /\$\{WAVE_PLUGIN_ROOT\}/g,
450
+ skill.pluginRoot,
451
+ );
452
+ }
453
+
446
454
  return header + description + skillPath + mainContent;
447
455
  }
448
456
 
@@ -493,6 +501,7 @@ export class SkillManager extends EventEmitter {
493
501
  disableModelInvocation: skill.disableModelInvocation,
494
502
  userInvocable: skill.userInvocable,
495
503
  pluginName,
504
+ pluginRoot: skill.pluginRoot,
496
505
  };
497
506
  // Update the skill object itself to have the namespaced name
498
507
  skill.name = namespacedName;
@@ -248,6 +248,8 @@ export class SlashCommandManager {
248
248
  },
249
249
  );
250
250
 
251
+ // Show loading while subagent runs
252
+ this.aiManager.setIsLoading(true);
251
253
  try {
252
254
  const result = await this.subagentManager.executeAgent(
253
255
  instance,
@@ -261,7 +263,11 @@ export class SlashCommandManager {
261
263
  messageId,
262
264
  result,
263
265
  stage: "end",
266
+ success: true,
264
267
  });
268
+
269
+ // Trigger AI to process the tool result
270
+ await this.aiManager.sendAIMessage();
265
271
  } finally {
266
272
  this.subagentManager.cleanupInstance(instance.subagentId);
267
273
  }
@@ -271,6 +277,7 @@ export class SlashCommandManager {
271
277
  id: toolBlockId,
272
278
  messageId,
273
279
  stage: "end",
280
+ success: false,
274
281
  error: error instanceof Error ? error.message : String(error),
275
282
  });
276
283
  throw error; // Re-throw to be caught by outer catch for logging/error block
@@ -279,6 +286,7 @@ export class SlashCommandManager {
279
286
  }
280
287
 
281
288
  // Non-forked skill: execute and trigger AI response
289
+ this.aiManager.setIsLoading(true);
282
290
  const result = await this.skillManager.executeSkill({
283
291
  skill_name: skill.name,
284
292
  args,
@@ -296,10 +304,9 @@ export class SlashCommandManager {
296
304
  allowedRules: result.allowedTools,
297
305
  });
298
306
  } catch (error) {
299
- logger?.error(
300
- `Failed to execute skill command '${skill.name}':`,
301
- error,
302
- );
307
+ this.aiManager.setIsLoading(false);
308
+
309
+ logger?.error(error);
303
310
  this.messageManager.addErrorBlock(
304
311
  `Failed to execute skill command '${skill.name}': ${
305
312
  error instanceof Error ? error.message : String(error)
@@ -490,6 +497,9 @@ export class SlashCommandManager {
490
497
  args?: string,
491
498
  ): Promise<void> {
492
499
  try {
500
+ // Set loading early so UI shows feedback during bash execution
501
+ this.aiManager.setIsLoading(true);
502
+
493
503
  // Parse bash commands from the content
494
504
  const { commands, processedContent } = parseBashCommands(content);
495
505
 
@@ -519,6 +529,8 @@ export class SlashCommandManager {
519
529
  allowedRules: config?.allowedTools,
520
530
  });
521
531
  } catch (error) {
532
+ this.aiManager.setIsLoading(false);
533
+
522
534
  logger?.error(
523
535
  `Failed to execute custom command '${commandName}':`,
524
536
  error,
@@ -243,7 +243,6 @@ export class SubagentManager {
243
243
  const parentPermissionManager =
244
244
  this.container.get<PermissionManager>("PermissionManager");
245
245
  const subagentPermissionManager = new PermissionManager(subagentContainer, {
246
- workdir: this.workdir,
247
246
  configuredPermissionMode:
248
247
  parameters.permissionModeOverride ??
249
248
  parentPermissionManager?.getConfiguredPermissionMode(),
@@ -263,6 +262,15 @@ export class SubagentManager {
263
262
  });
264
263
  subagentContainer.register("PermissionManager", subagentPermissionManager);
265
264
 
265
+ // Register the permission mode override in the subagent container so it
266
+ // shadows the inherited parent value during tool execution
267
+ if (parameters.permissionModeOverride) {
268
+ subagentContainer.register(
269
+ "PermissionMode",
270
+ parameters.permissionModeOverride,
271
+ );
272
+ }
273
+
266
274
  // Track this subagent's PermissionManager for rule sync
267
275
  this.subagentPermissionManagers.set(subagentId, subagentPermissionManager);
268
276
 
@@ -399,6 +407,7 @@ export class SubagentManager {
399
407
  instance.backgroundTaskId = taskId;
400
408
 
401
409
  // Execute in background
410
+ // Note: notification enqueueing is handled by internalExecute when instance.backgroundTaskId is set
402
411
  (async () => {
403
412
  try {
404
413
  const result = await this.internalExecute(
@@ -413,17 +422,6 @@ export class SubagentManager {
413
422
  task.endTime = Date.now();
414
423
  task.runtime = task.endTime - startTime;
415
424
  }
416
-
417
- // Enqueue completion notification
418
- const notificationQueue = this.container.has("NotificationQueue")
419
- ? this.container.get<NotificationQueue>("NotificationQueue")
420
- : undefined;
421
- if (notificationQueue) {
422
- const summary = `Agent task "${instance.description}" completed`;
423
- notificationQueue.enqueue(
424
- `<task-notification>\n<task-id>${taskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`,
425
- );
426
- }
427
425
  } catch (error) {
428
426
  const task = backgroundTaskManager?.getTask(taskId);
429
427
  if (task) {
@@ -433,19 +431,6 @@ export class SubagentManager {
433
431
  task.endTime = Date.now();
434
432
  task.runtime = task.endTime - startTime;
435
433
  }
436
-
437
- // Enqueue error notification
438
- const notificationQueue = this.container.has("NotificationQueue")
439
- ? this.container.get<NotificationQueue>("NotificationQueue")
440
- : undefined;
441
- if (notificationQueue) {
442
- const errorMsg =
443
- error instanceof Error ? error.message : String(error);
444
- const summary = `Agent task "${instance.description}" failed: ${errorMsg}`;
445
- notificationQueue.enqueue(
446
- `<task-notification>\n<task-id>${taskId}</task-id>\n<task-type>agent</task-type>\n<status>failed</status>\n<summary>${summary}</summary>\n</task-notification>`,
447
- );
448
- }
449
434
  }
450
435
  })();
451
436
 
@@ -160,6 +160,7 @@ export class AutoMemoryService {
160
160
  "Grep",
161
161
  `Write(${memoryDir}/**/*)`,
162
162
  `Edit(${memoryDir}/**/*)`,
163
+ `Bash(rm ${memoryDir}/**/*)`,
163
164
  ],
164
165
  model: "fastModel", // Use fast model for background tasks to reduce latency and cost
165
166
  permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user