wave-agent-sdk 0.10.1 → 0.10.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 (76) hide show
  1. package/dist/agent.d.ts +6 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +2 -0
  4. package/dist/core/session.d.ts +1 -1
  5. package/dist/core/session.d.ts.map +1 -1
  6. package/dist/core/session.js +1 -1
  7. package/dist/managers/backgroundTaskManager.d.ts +3 -0
  8. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  9. package/dist/managers/backgroundTaskManager.js +3 -0
  10. package/dist/managers/permissionManager.d.ts.map +1 -1
  11. package/dist/managers/permissionManager.js +33 -4
  12. package/dist/managers/skillManager.d.ts +17 -2
  13. package/dist/managers/skillManager.d.ts.map +1 -1
  14. package/dist/managers/skillManager.js +93 -21
  15. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  16. package/dist/managers/slashCommandManager.js +7 -0
  17. package/dist/prompts/index.d.ts +1 -1
  18. package/dist/prompts/index.d.ts.map +1 -1
  19. package/dist/prompts/index.js +1 -0
  20. package/dist/services/configurationService.d.ts.map +1 -1
  21. package/dist/services/configurationService.js +1 -0
  22. package/dist/services/fileWatcher.d.ts.map +1 -1
  23. package/dist/services/fileWatcher.js +27 -15
  24. package/dist/services/session.d.ts +9 -2
  25. package/dist/services/session.d.ts.map +1 -1
  26. package/dist/services/session.js +77 -7
  27. package/dist/tools/bashTool.js +3 -3
  28. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  29. package/dist/tools/exitPlanMode.js +3 -1
  30. package/dist/tools/grepTool.d.ts.map +1 -1
  31. package/dist/tools/grepTool.js +1 -1
  32. package/dist/tools/lspTool.d.ts.map +1 -1
  33. package/dist/tools/lspTool.js +47 -15
  34. package/dist/tools/taskOutputTool.d.ts.map +1 -1
  35. package/dist/tools/taskOutputTool.js +22 -5
  36. package/dist/tools/types.d.ts +1 -0
  37. package/dist/tools/types.d.ts.map +1 -1
  38. package/dist/types/agent.d.ts +2 -0
  39. package/dist/types/agent.d.ts.map +1 -1
  40. package/dist/types/configuration.d.ts +2 -0
  41. package/dist/types/configuration.d.ts.map +1 -1
  42. package/dist/types/skills.d.ts +4 -2
  43. package/dist/types/skills.d.ts.map +1 -1
  44. package/dist/utils/configPaths.d.ts +6 -0
  45. package/dist/utils/configPaths.d.ts.map +1 -1
  46. package/dist/utils/configPaths.js +23 -2
  47. package/dist/utils/containerSetup.d.ts.map +1 -1
  48. package/dist/utils/containerSetup.js +4 -1
  49. package/dist/utils/gitUtils.d.ts +6 -0
  50. package/dist/utils/gitUtils.d.ts.map +1 -1
  51. package/dist/utils/gitUtils.js +22 -0
  52. package/package.json +1 -1
  53. package/src/agent.ts +18 -2
  54. package/src/builtin-skills/settings/HOOKS.md +95 -0
  55. package/src/builtin-skills/settings/SKILL.md +86 -0
  56. package/src/core/session.ts +1 -0
  57. package/src/managers/backgroundTaskManager.ts +11 -1
  58. package/src/managers/permissionManager.ts +33 -4
  59. package/src/managers/skillManager.ts +111 -25
  60. package/src/managers/slashCommandManager.ts +8 -0
  61. package/src/prompts/index.ts +1 -0
  62. package/src/services/configurationService.ts +1 -0
  63. package/src/services/fileWatcher.ts +33 -17
  64. package/src/services/session.ts +83 -7
  65. package/src/tools/bashTool.ts +3 -3
  66. package/src/tools/exitPlanMode.ts +3 -1
  67. package/src/tools/grepTool.ts +2 -1
  68. package/src/tools/lspTool.ts +99 -9
  69. package/src/tools/taskOutputTool.ts +25 -6
  70. package/src/tools/types.ts +2 -0
  71. package/src/types/agent.ts +2 -0
  72. package/src/types/configuration.ts +2 -0
  73. package/src/types/skills.ts +4 -2
  74. package/src/utils/configPaths.ts +28 -2
  75. package/src/utils/containerSetup.ts +4 -1
  76. package/src/utils/gitUtils.ts +22 -0
@@ -95,7 +95,7 @@ export class FileWatcherService extends EventEmitter {
95
95
  await this.initializeWatcher(entry);
96
96
  } catch (error) {
97
97
  this.logger?.error(
98
- `Live Config: Failed to watch file ${path}: ${(error as Error).message}`,
98
+ `FileWatcher: Failed to watch file ${path}: ${(error as Error).message}`,
99
99
  );
100
100
  throw error;
101
101
  }
@@ -117,7 +117,7 @@ export class FileWatcherService extends EventEmitter {
117
117
  this.watchers.delete(path);
118
118
  } catch (error) {
119
119
  this.logger?.warn(
120
- `Live Config: Error unwatching file ${path}: ${(error as Error).message}`,
120
+ `FileWatcher: Error unwatching file ${path}: ${(error as Error).message}`,
121
121
  );
122
122
  }
123
123
  }
@@ -204,7 +204,7 @@ export class FileWatcherService extends EventEmitter {
204
204
  entry.lastError = (error as Error).message;
205
205
 
206
206
  this.logger?.error(
207
- `Live Config: Failed to initialize watcher for ${entry.path}: ${(error as Error).message}`,
207
+ `FileWatcher: Failed to initialize watcher for ${entry.path}: ${(error as Error).message}`,
208
208
  );
209
209
 
210
210
  // Try fallback polling if not already using it
@@ -241,9 +241,17 @@ export class FileWatcherService extends EventEmitter {
241
241
  this.handleFileEvent("delete", filePath);
242
242
  });
243
243
 
244
+ this.globalWatcher.on("addDir", (dirPath: string) => {
245
+ this.handleFileEvent("create", dirPath);
246
+ });
247
+
248
+ this.globalWatcher.on("unlinkDir", (dirPath: string) => {
249
+ this.handleFileEvent("delete", dirPath);
250
+ });
251
+
244
252
  this.globalWatcher.on("error", (err: unknown) => {
245
253
  const error = err instanceof Error ? err : new Error(String(err));
246
- this.logger?.error(`Live Config: File watcher error: ${error.message}`);
254
+ this.logger?.error(`FileWatcher: File watcher error: ${error.message}`);
247
255
  this.emit("watcherError", error);
248
256
  });
249
257
  }
@@ -253,9 +261,6 @@ export class FileWatcherService extends EventEmitter {
253
261
  filePath: string,
254
262
  stats?: { size?: number },
255
263
  ): void {
256
- const entry = this.watchers.get(filePath);
257
- if (!entry) return;
258
-
259
264
  const event: FileWatchEvent = {
260
265
  type,
261
266
  path: filePath,
@@ -263,16 +268,27 @@ export class FileWatcherService extends EventEmitter {
263
268
  size: stats?.size,
264
269
  };
265
270
 
266
- entry.lastEvent = event.timestamp;
267
-
268
- // Notify all callbacks for this file
269
- for (const callback of entry.callbacks) {
270
- try {
271
- callback(event);
272
- } catch (error) {
273
- this.logger?.error(
274
- `Live Config: Error in file watch callback for ${filePath}: ${(error as Error).message}`,
275
- );
271
+ // Notify all watchers that match the path or are parents of the path
272
+ for (const [watchedPath, entry] of this.watchers.entries()) {
273
+ if (
274
+ filePath === watchedPath ||
275
+ filePath.startsWith(watchedPath + "/") ||
276
+ // Handle cases where the watched path might be a file and we get an event for it
277
+ // (already covered by filePath === watchedPath)
278
+ false
279
+ ) {
280
+ entry.lastEvent = event.timestamp;
281
+
282
+ // Notify all callbacks for this watcher
283
+ for (const callback of entry.callbacks) {
284
+ try {
285
+ callback(event);
286
+ } catch (error) {
287
+ this.logger?.error(
288
+ `FileWatcher: Error in file watch callback for ${watchedPath} (event on ${filePath}): ${(error as Error).message}`,
289
+ );
290
+ }
291
+ }
276
292
  }
277
293
  }
278
294
  }
@@ -423,11 +423,17 @@ export async function listSessionsFromJsonl(
423
423
 
424
424
  // Try to read from index first
425
425
  const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
426
+ const sevenDaysAgo = new Date();
427
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
428
+
426
429
  try {
427
430
  const indexContent = await fs.readFile(indexPath, "utf8");
428
431
  const index = JSON.parse(indexContent) as SessionIndex;
429
432
  const sessions: SessionMetadata[] = Object.entries(index.sessions)
430
- .filter(([, meta]) => meta.sessionType === "main")
433
+ .filter(([, meta]) => {
434
+ const lastActiveAt = new Date(meta.lastActiveAt);
435
+ return meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo;
436
+ })
431
437
  .map(([id, meta]) => ({
432
438
  id,
433
439
  ...meta,
@@ -492,6 +498,10 @@ export async function listSessionsFromJsonl(
492
498
  lastActiveAt = stats.mtime;
493
499
  }
494
500
 
501
+ if (lastActiveAt < sevenDaysAgo) {
502
+ continue;
503
+ }
504
+
495
505
  // Return inline object for performance (no interface instantiation overhead)
496
506
  const sessionMeta: SessionMetadata = {
497
507
  id: sessionId,
@@ -550,6 +560,68 @@ export async function listSessionsFromJsonl(
550
560
  }
551
561
  }
552
562
 
563
+ /**
564
+ * List all sessions across all project directories
565
+ *
566
+ * @returns Promise that resolves to array of session metadata objects
567
+ */
568
+ export async function listAllSessions(): Promise<SessionMetadata[]> {
569
+ try {
570
+ const baseDir = SESSION_DIR;
571
+ let projectDirs: string[];
572
+ try {
573
+ projectDirs = await fs.readdir(baseDir);
574
+ } catch (error) {
575
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
576
+ return [];
577
+ }
578
+ throw error;
579
+ }
580
+
581
+ const allSessions: SessionMetadata[] = [];
582
+ const sevenDaysAgo = new Date();
583
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
584
+
585
+ for (const projectDirName of projectDirs) {
586
+ const projectPath = join(baseDir, projectDirName);
587
+ try {
588
+ const stat = await fs.stat(projectPath);
589
+ if (!stat.isDirectory()) {
590
+ continue;
591
+ }
592
+
593
+ // Try to read from index first
594
+ const indexPath = join(projectPath, SESSION_INDEX_FILENAME);
595
+ try {
596
+ const indexContent = await fs.readFile(indexPath, "utf8");
597
+ const index = JSON.parse(indexContent) as SessionIndex;
598
+ for (const [id, meta] of Object.entries(index.sessions)) {
599
+ const lastActiveAt = new Date(meta.lastActiveAt);
600
+ if (meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo) {
601
+ allSessions.push({
602
+ id,
603
+ ...meta,
604
+ lastActiveAt,
605
+ });
606
+ }
607
+ }
608
+ } catch {
609
+ // If index fails, we skip this project directory
610
+ // In the future, we could scan for .jsonl files here
611
+ }
612
+ } catch {
613
+ // Skip if stat fails
614
+ }
615
+ }
616
+
617
+ return allSessions.sort(
618
+ (a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
619
+ );
620
+ } catch (error) {
621
+ throw new Error(`Failed to list all sessions: ${error}`);
622
+ }
623
+ }
624
+
553
625
  /**
554
626
  * Clean up expired sessions older than 14 days based on file modification time
555
627
  *
@@ -840,19 +912,23 @@ export async function deleteSession(
840
912
  }
841
913
 
842
914
  /**
843
- * Truncate content to a maximum length, adding ellipsis if truncated
915
+ * Truncate content to a maximum length, adding ellipsis if truncated.
916
+ * Also replaces real newlines with the literal string "\n".
844
917
  * @param content - The content to truncate
845
- * @param maxLength - Maximum length before truncation (default: 30)
918
+ * @param maxLength - Maximum length before truncation (default: 100)
846
919
  * @returns Truncated content with ellipsis if needed
847
920
  */
848
921
  export function truncateContent(
849
922
  content: string,
850
- maxLength: number = 30,
923
+ maxLength: number = 100,
851
924
  ): string {
852
- if (content.length <= maxLength) {
853
- return content;
925
+ // Replace real newlines with literal "\n"
926
+ const singleLineContent = content.replace(/\n/g, "\\n");
927
+
928
+ if (singleLineContent.length <= maxLength) {
929
+ return singleLineContent;
854
930
  }
855
- return content.substring(0, maxLength) + "...";
931
+ return singleLineContent.substring(0, maxLength) + "...";
856
932
  }
857
933
 
858
934
  /**
@@ -77,10 +77,10 @@ Usage notes:
77
77
  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
78
78
  - If the output exceeds ${MAX_OUTPUT_LENGTH} characters, output will be truncated before being returned to you.
79
79
  - You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${BASH_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
80
- - Avoid using ${BASH_TOOL_NAME} with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
80
+ - Avoid using ${BASH_TOOL_NAME} with the \`find\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
81
81
  - File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)
82
- - Content search: Use ${GREP_TOOL_NAME} (NOT grep or rg)
83
- - Read files: Use ${READ_TOOL_NAME} (NOT cat/head/tail)
82
+ - Content search: Use ${GREP_TOOL_NAME}
83
+ - Read files: Use ${READ_TOOL_NAME}
84
84
  - Edit files: Use ${EDIT_TOOL_NAME} (NOT sed/awk)
85
85
  - Write files: Use ${WRITE_TOOL_NAME} (NOT echo >/cat <<EOF)
86
86
  - Communication: Output text directly (NOT echo/printf)
@@ -85,7 +85,9 @@ Ensure your plan is complete and unambiguous:
85
85
  EXIT_PLAN_MODE_TOOL_NAME,
86
86
  context.permissionMode || "plan",
87
87
  context.canUseToolCallback,
88
- { plan_content: planContent },
88
+ {
89
+ plan_content: planContent,
90
+ },
89
91
  context.toolCallId,
90
92
  );
91
93
 
@@ -212,7 +212,8 @@ export const grepTool: ToolPlugin = {
212
212
  if (!output) {
213
213
  return {
214
214
  success: true,
215
- content: "No matches found",
215
+ content:
216
+ "No matches found. Suggestion: specify the 'path' field to search in ignored or other directories (e.g., 'node_modules'), as the default search path is the current working directory and respects .gitignore.",
216
217
  shortResult: "No matches found",
217
218
  };
218
219
  }
@@ -1,5 +1,6 @@
1
1
  import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
- import { relative } from "path";
2
+ import { relative, join, isAbsolute } from "path";
3
+ import * as fs from "fs";
3
4
  import { logger } from "../utils/globalLogger.js";
4
5
  import { getDisplayPath } from "../utils/path.js";
5
6
  import { LSP_TOOL_NAME } from "../constants/tools.js";
@@ -50,6 +51,43 @@ function formatUri(uri: string, workdir?: string): string {
50
51
  return path;
51
52
  }
52
53
 
54
+ /**
55
+ * Formats an error message when no results are found, including context from the file
56
+ */
57
+ function formatNoResultError(
58
+ message: string,
59
+ filePath: string,
60
+ line: number,
61
+ character: number,
62
+ workdir?: string,
63
+ ): string {
64
+ let absolutePath = filePath;
65
+ if (!isAbsolute(filePath) && workdir) {
66
+ absolutePath = join(workdir, filePath);
67
+ }
68
+
69
+ let lineContent = "";
70
+ try {
71
+ const content = fs.readFileSync(absolutePath, "utf-8");
72
+ const lines = content.split("\n");
73
+ if (line > 0 && line <= lines.length) {
74
+ lineContent = lines[line - 1];
75
+ }
76
+ } catch {
77
+ // Ignore file read errors
78
+ }
79
+
80
+ const displayPath = getDisplayPath(filePath, workdir || "");
81
+ const baseMessage = `${message}\n\nPlease check if the character offset is correct and points to a valid symbol.`;
82
+
83
+ if (lineContent) {
84
+ const pointer = " ".repeat(Math.max(0, character - 1)) + "^";
85
+ return `${message}\n\nContext at ${displayPath}:${line}:${character}:\n${lineContent}\n${pointer}\n\nPlease check if the character offset is correct and points to a valid symbol.`;
86
+ }
87
+
88
+ return baseMessage;
89
+ }
90
+
53
91
  /**
54
92
  * Groups items by their URI
55
93
  */
@@ -109,10 +147,19 @@ function isLocationLink(loc: Location | LocationLink): loc is LocationLink {
109
147
  */
110
148
  function formatGoToDefinitionResult(
111
149
  result: Location | Location[] | LocationLink | LocationLink[] | null,
150
+ filePath: string,
151
+ line: number,
152
+ character: number,
153
+ operation: string,
112
154
  workdir?: string,
113
155
  ): string {
156
+ const message =
157
+ operation === "goToImplementation"
158
+ ? "No implementation found. This may occur if the cursor is not on a symbol, or if the implementation is in an external library not indexed by the LSP server."
159
+ : "No definition found. This may occur if the cursor is not on a symbol, or if the definition is in an external library not indexed by the LSP server.";
160
+
114
161
  if (!result) {
115
- return "No definition found. This may occur if the cursor is not on a symbol, or if the definition is in an external library not indexed by the LSP server.";
162
+ return formatNoResultError(message, filePath, line, character, workdir);
116
163
  }
117
164
 
118
165
  if (Array.isArray(result)) {
@@ -122,7 +169,7 @@ function formatGoToDefinitionResult(
122
169
  const validLocations = locations.filter((loc) => loc && loc.uri);
123
170
 
124
171
  if (validLocations.length === 0) {
125
- return "No definition found. This may occur if the cursor is not on a symbol, or if the definition is in an external library not indexed by the LSP server.";
172
+ return formatNoResultError(message, filePath, line, character, workdir);
126
173
  }
127
174
 
128
175
  if (validLocations.length === 1) {
@@ -150,15 +197,21 @@ function formatGoToDefinitionResult(
150
197
  */
151
198
  function formatFindReferencesResult(
152
199
  result: Location[] | null,
200
+ filePath: string,
201
+ line: number,
202
+ character: number,
153
203
  workdir?: string,
154
204
  ): string {
205
+ const message =
206
+ "No references found. This may occur if the symbol has no usages, or if the LSP server has not fully indexed the workspace.";
207
+
155
208
  if (!result || result.length === 0) {
156
- return "No references found. This may occur if the symbol has no usages, or if the LSP server has not fully indexed the workspace.";
209
+ return formatNoResultError(message, filePath, line, character, workdir);
157
210
  }
158
211
 
159
212
  const validLocations = result.filter((loc) => loc && loc.uri);
160
213
  if (validLocations.length === 0) {
161
- return "No references found. This may occur if the symbol has no usages, or if the LSP server has not fully indexed the workspace.";
214
+ return formatNoResultError(message, filePath, line, character, workdir);
162
215
  }
163
216
 
164
217
  if (validLocations.length === 1) {
@@ -224,9 +277,21 @@ function formatHoverContents(contents: Hover["contents"]): string {
224
277
  /**
225
278
  * Formats the result of a hover operation
226
279
  */
227
- function formatHoverResult(result: Hover | null): string {
280
+ function formatHoverResult(
281
+ result: Hover | null,
282
+ filePath: string,
283
+ line: number,
284
+ character: number,
285
+ workdir?: string,
286
+ ): string {
228
287
  if (!result) {
229
- return "No hover information available. This may occur if the cursor is not on a symbol, or if the LSP server has not fully indexed the file.";
288
+ return formatNoResultError(
289
+ "No hover information available. This may occur if the cursor is not on a symbol, or if the LSP server has not fully indexed the file.",
290
+ filePath,
291
+ line,
292
+ character,
293
+ workdir,
294
+ );
230
295
  }
231
296
 
232
297
  const contents = formatHoverContents(result.contents);
@@ -428,10 +493,19 @@ function formatCallHierarchyItem(
428
493
  */
429
494
  function formatPrepareCallHierarchyResult(
430
495
  result: CallHierarchyItem[] | null,
496
+ filePath: string,
497
+ line: number,
498
+ character: number,
431
499
  workdir?: string,
432
500
  ): string {
433
501
  if (!result || result.length === 0) {
434
- return "No call hierarchy item found at this position";
502
+ return formatNoResultError(
503
+ "No call hierarchy item found at this position",
504
+ filePath,
505
+ line,
506
+ character,
507
+ workdir,
508
+ );
435
509
  }
436
510
 
437
511
  if (result.length === 1) {
@@ -719,17 +793,30 @@ Note: LSP servers must be configured for the file type. If no server is availabl
719
793
  | LocationLink
720
794
  | LocationLink[]
721
795
  | null,
796
+ filePath,
797
+ line,
798
+ character,
799
+ operation,
722
800
  context.workdir,
723
801
  );
724
802
  break;
725
803
  case "findReferences":
726
804
  formattedContent = formatFindReferencesResult(
727
805
  rawResult as Location[] | null,
806
+ filePath,
807
+ line,
808
+ character,
728
809
  context.workdir,
729
810
  );
730
811
  break;
731
812
  case "hover":
732
- formattedContent = formatHoverResult(rawResult as Hover | null);
813
+ formattedContent = formatHoverResult(
814
+ rawResult as Hover | null,
815
+ filePath,
816
+ line,
817
+ character,
818
+ context.workdir,
819
+ );
733
820
  break;
734
821
  case "documentSymbol":
735
822
  formattedContent = formatDocumentSymbolResult(
@@ -746,6 +833,9 @@ Note: LSP servers must be configured for the file type. If no server is availabl
746
833
  case "prepareCallHierarchy":
747
834
  formattedContent = formatPrepareCallHierarchyResult(
748
835
  rawResult as CallHierarchyItem[] | null,
836
+ filePath,
837
+ line,
838
+ character,
749
839
  context.workdir,
750
840
  );
751
841
  break;
@@ -82,16 +82,35 @@ export const taskOutputTool: ToolPlugin = {
82
82
  }
83
83
 
84
84
  const finalContent = content || "No output available";
85
- const processedContent =
86
- finalContent.length > MAX_OUTPUT_LENGTH
87
- ? finalContent.substring(0, MAX_OUTPUT_LENGTH) +
88
- "\n\n... (output truncated)"
89
- : finalContent;
85
+ let processedContent = finalContent;
86
+ let isTruncated = false;
87
+
88
+ if (finalContent.length > MAX_OUTPUT_LENGTH) {
89
+ processedContent =
90
+ finalContent.substring(0, MAX_OUTPUT_LENGTH) +
91
+ "\n\n... (output truncated)";
92
+ isTruncated = true;
93
+ }
94
+
95
+ if (isTruncated && output.outputPath) {
96
+ processedContent += `\n\nFull output available at: ${output.outputPath}`;
97
+ }
98
+
99
+ let shortResult = `${taskId}: ${output.status}`;
100
+ if (output.status === "failed" && output.exitCode !== undefined) {
101
+ shortResult += ` (exit code: ${output.exitCode})`;
102
+ }
90
103
 
91
104
  return {
92
105
  success: true,
93
106
  content: processedContent,
94
- shortResult: `${taskId}: ${output.status}`,
107
+ shortResult,
108
+ metadata: {
109
+ outputPath: output.outputPath,
110
+ status: output.status,
111
+ type: output.type,
112
+ exitCode: output.exitCode,
113
+ },
95
114
  };
96
115
  };
97
116
 
@@ -49,6 +49,8 @@ export interface ToolResult {
49
49
  }>;
50
50
  // Whether the tool was manually backgrounded by the user (e.g. via Ctrl-B)
51
51
  isManuallyBackgrounded?: boolean;
52
+ // Optional metadata for the tool result
53
+ metadata?: Record<string, unknown>;
52
54
  }
53
55
 
54
56
  export interface ToolContext {
@@ -65,6 +65,8 @@ export interface AgentOptions {
65
65
  worktreeName?: string;
66
66
  /**Whether this is a newly created worktree */
67
67
  isNewWorktree?: boolean;
68
+ /**Whether to watch for skill changes - defaults to true */
69
+ watchSkills?: boolean;
68
70
  }
69
71
 
70
72
  export interface AgentCallbacks
@@ -91,6 +91,8 @@ export interface ConfigurationPaths {
91
91
  userPaths: string[];
92
92
  /** Project-specific configuration file paths in priority order */
93
93
  projectPaths: string[];
94
+ /** Builtin configuration file paths */
95
+ builtinPaths: string[];
94
96
  /** All configuration paths combined */
95
97
  allPaths: string[];
96
98
  /** Only the paths that actually exist on the filesystem */
@@ -6,7 +6,7 @@
6
6
  export interface SkillMetadata {
7
7
  name: string;
8
8
  description: string;
9
- type: "personal" | "project";
9
+ type: "personal" | "project" | "builtin";
10
10
  skillPath: string;
11
11
  allowedTools?: string[];
12
12
  context?: "fork";
@@ -37,7 +37,7 @@ export interface SkillFrontmatter {
37
37
  }
38
38
 
39
39
  export interface SkillCollection {
40
- type: "personal" | "project";
40
+ type: "personal" | "project" | "builtin";
41
41
  basePath: string;
42
42
  skills: Map<string, SkillMetadata>;
43
43
  errors: SkillError[];
@@ -57,6 +57,7 @@ export interface SkillValidationResult {
57
57
  export interface SkillDiscoveryResult {
58
58
  personalSkills: Map<string, SkillMetadata>;
59
59
  projectSkills: Map<string, SkillMetadata>;
60
+ builtinSkills: Map<string, SkillMetadata>;
60
61
  errors: SkillError[];
61
62
  }
62
63
 
@@ -73,6 +74,7 @@ export interface SkillManagerOptions {
73
74
  personalSkillsPath?: string;
74
75
  scanTimeout?: number;
75
76
  workdir?: string;
77
+ watch?: boolean;
76
78
  }
77
79
 
78
80
  export interface ParsedSkillFile {
@@ -10,9 +10,29 @@
10
10
  * - Project configs override user configs (existing behavior)
11
11
  */
12
12
 
13
- import { join } from "path";
13
+ import { join, dirname } from "path";
14
14
  import { homedir } from "os";
15
15
  import { existsSync } from "fs";
16
+ import { fileURLToPath } from "url";
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ /**
22
+ * Get the builtin skills directory path
23
+ */
24
+ export function getBuiltinSkillsDir(): string {
25
+ // In development, it's in src/builtin-skills
26
+ // In production (dist), it should be in dist/builtin-skills
27
+ // We'll look for it relative to this file
28
+ const devPath = join(__dirname, "..", "builtin-skills");
29
+ const prodPath = join(__dirname, "builtin-skills");
30
+
31
+ if (existsSync(devPath)) {
32
+ return devPath;
33
+ }
34
+ return prodPath;
35
+ }
16
36
 
17
37
  /**
18
38
  * Get the user-specific configuration file path (legacy function)
@@ -62,15 +82,18 @@ export function getProjectConfigPaths(workdir: string): string[] {
62
82
  export function getAllConfigPaths(workdir: string): {
63
83
  userPaths: string[];
64
84
  projectPaths: string[];
85
+ builtinPaths: string[];
65
86
  allPaths: string[];
66
87
  } {
67
88
  const userPaths = getUserConfigPaths();
68
89
  const projectPaths = getProjectConfigPaths(workdir);
90
+ const builtinPaths = [join(getBuiltinSkillsDir(), "settings", "SKILL.md")];
69
91
 
70
92
  return {
71
93
  userPaths,
72
94
  projectPaths,
73
- allPaths: [...userPaths, ...projectPaths],
95
+ builtinPaths,
96
+ allPaths: [...userPaths, ...projectPaths, ...builtinPaths],
74
97
  };
75
98
  }
76
99
 
@@ -81,17 +104,20 @@ export function getAllConfigPaths(workdir: string): {
81
104
  export function getExistingConfigPaths(workdir: string): {
82
105
  userPaths: string[];
83
106
  projectPaths: string[];
107
+ builtinPaths: string[];
84
108
  existingPaths: string[];
85
109
  } {
86
110
  const allPaths = getAllConfigPaths(workdir);
87
111
 
88
112
  const existingUserPaths = allPaths.userPaths.filter(existsSync);
89
113
  const existingProjectPaths = allPaths.projectPaths.filter(existsSync);
114
+ const existingBuiltinPaths = allPaths.builtinPaths.filter(existsSync);
90
115
  const allExistingPaths = allPaths.allPaths.filter(existsSync);
91
116
 
92
117
  return {
93
118
  userPaths: existingUserPaths,
94
119
  projectPaths: existingProjectPaths,
120
+ builtinPaths: existingBuiltinPaths,
95
121
  existingPaths: allExistingPaths,
96
122
  };
97
123
  }
@@ -147,7 +147,10 @@ export function setupAgentContainer(
147
147
  const hookManager = new HookManager(container, workdir);
148
148
  container.register("HookManager", hookManager);
149
149
 
150
- const skillManager = new SkillManager(container, { workdir });
150
+ const skillManager = new SkillManager(container, {
151
+ workdir,
152
+ watch: options.watchSkills ?? true,
153
+ });
151
154
  container.register("SkillManager", skillManager);
152
155
 
153
156
  container.register(
@@ -59,6 +59,28 @@ export function getGitCommonDir(cwd: string): string {
59
59
  }
60
60
  }
61
61
 
62
+ /**
63
+ * Get the main repository root directory (the first worktree in the list)
64
+ * @param cwd Working directory
65
+ * @returns Main repository root path
66
+ */
67
+ export function getGitMainRepoRoot(cwd: string): string {
68
+ try {
69
+ const output = execSync("git worktree list --porcelain", {
70
+ cwd,
71
+ encoding: "utf8",
72
+ stdio: ["ignore", "pipe", "ignore"],
73
+ }).trim();
74
+ const lines = output.split("\n");
75
+ if (lines.length > 0 && lines[0].startsWith("worktree ")) {
76
+ return lines[0].substring("worktree ".length).trim();
77
+ }
78
+ return getGitRepoRoot(cwd);
79
+ } catch {
80
+ return getGitRepoRoot(cwd);
81
+ }
82
+ }
83
+
62
84
  /**
63
85
  * Get the default remote branch (e.g., origin/main)
64
86
  * @param cwd Working directory