@clinebot/core 0.0.11 → 0.0.13

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-config-loader.d.ts +1 -1
  3. package/dist/agents/agent-config-parser.d.ts +5 -2
  4. package/dist/agents/index.d.ts +1 -1
  5. package/dist/agents/plugin-config-loader.d.ts +4 -0
  6. package/dist/agents/plugin-loader.d.ts +1 -0
  7. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  8. package/dist/agents/plugin-sandbox.d.ts +4 -0
  9. package/dist/index.node.d.ts +5 -0
  10. package/dist/index.node.js +685 -413
  11. package/dist/runtime/commands.d.ts +11 -0
  12. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  13. package/dist/runtime/skills.d.ts +13 -0
  14. package/dist/session/default-session-manager.d.ts +5 -0
  15. package/dist/session/session-config-builder.d.ts +4 -1
  16. package/dist/session/session-manager.d.ts +1 -0
  17. package/dist/session/session-service.d.ts +22 -22
  18. package/dist/session/unified-session-persistence-service.d.ts +12 -6
  19. package/dist/session/utils/helpers.d.ts +2 -2
  20. package/dist/session/utils/types.d.ts +9 -0
  21. package/dist/tools/definitions.d.ts +2 -2
  22. package/dist/tools/presets.d.ts +3 -3
  23. package/dist/tools/schemas.d.ts +15 -14
  24. package/dist/types/config.d.ts +5 -0
  25. package/dist/types/events.d.ts +22 -0
  26. package/package.json +5 -4
  27. package/src/agents/agent-config-loader.test.ts +2 -0
  28. package/src/agents/agent-config-loader.ts +1 -0
  29. package/src/agents/agent-config-parser.ts +12 -5
  30. package/src/agents/index.ts +1 -0
  31. package/src/agents/plugin-config-loader.test.ts +49 -0
  32. package/src/agents/plugin-config-loader.ts +10 -73
  33. package/src/agents/plugin-loader.test.ts +127 -1
  34. package/src/agents/plugin-loader.ts +72 -5
  35. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  36. package/src/agents/plugin-sandbox.test.ts +198 -1
  37. package/src/agents/plugin-sandbox.ts +223 -353
  38. package/src/index.node.ts +14 -0
  39. package/src/runtime/commands.test.ts +98 -0
  40. package/src/runtime/commands.ts +83 -0
  41. package/src/runtime/hook-file-hooks.test.ts +1 -1
  42. package/src/runtime/hook-file-hooks.ts +16 -6
  43. package/src/runtime/index.ts +10 -0
  44. package/src/runtime/runtime-builder.test.ts +67 -0
  45. package/src/runtime/runtime-builder.ts +70 -16
  46. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  47. package/src/runtime/skills.ts +44 -0
  48. package/src/runtime/workflows.ts +20 -29
  49. package/src/session/default-session-manager.e2e.test.ts +52 -33
  50. package/src/session/default-session-manager.test.ts +453 -1
  51. package/src/session/default-session-manager.ts +210 -12
  52. package/src/session/rpc-session-service.ts +14 -96
  53. package/src/session/session-config-builder.ts +2 -0
  54. package/src/session/session-manager.ts +1 -0
  55. package/src/session/session-service.ts +127 -64
  56. package/src/session/session-team-coordination.ts +30 -0
  57. package/src/session/unified-session-persistence-service.test.ts +3 -3
  58. package/src/session/unified-session-persistence-service.ts +159 -141
  59. package/src/session/utils/helpers.ts +22 -41
  60. package/src/session/utils/types.ts +10 -0
  61. package/src/storage/sqlite-team-store.ts +16 -5
  62. package/src/tools/definitions.test.ts +137 -8
  63. package/src/tools/definitions.ts +115 -70
  64. package/src/tools/presets.test.ts +2 -3
  65. package/src/tools/presets.ts +3 -3
  66. package/src/tools/schemas.ts +28 -28
  67. package/src/types/config.ts +5 -0
  68. package/src/types/events.ts +23 -0
@@ -120,6 +120,8 @@ export class SqliteTeamStore implements TeamStore {
120
120
  }
121
121
 
122
122
  private ensureSchema(db: SqliteDb): void {
123
+ db.exec("PRAGMA journal_mode = WAL;");
124
+ db.exec("PRAGMA busy_timeout = 5000;");
123
125
  db.exec(`
124
126
  CREATE TABLE IF NOT EXISTS team_events (
125
127
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -130,16 +132,20 @@ export class SqliteTeamStore implements TeamStore {
130
132
  causation_id TEXT,
131
133
  correlation_id TEXT
132
134
  );
135
+ `);
136
+ db.exec(`
133
137
  CREATE INDEX IF NOT EXISTS idx_team_events_name_ts
134
138
  ON team_events(team_name, ts DESC);
135
-
139
+ `);
140
+ db.exec(`
136
141
  CREATE TABLE IF NOT EXISTS team_runtime_snapshot (
137
142
  team_name TEXT PRIMARY KEY,
138
143
  state_json TEXT NOT NULL,
139
144
  teammates_json TEXT NOT NULL,
140
145
  updated_at TEXT NOT NULL
141
146
  );
142
-
147
+ `);
148
+ db.exec(`
143
149
  CREATE TABLE IF NOT EXISTS team_tasks (
144
150
  team_name TEXT NOT NULL,
145
151
  task_id TEXT NOT NULL,
@@ -153,7 +159,8 @@ export class SqliteTeamStore implements TeamStore {
153
159
  updated_at TEXT NOT NULL,
154
160
  PRIMARY KEY(team_name, task_id)
155
161
  );
156
-
162
+ `);
163
+ db.exec(`
157
164
  CREATE TABLE IF NOT EXISTS team_runs (
158
165
  team_name TEXT NOT NULL,
159
166
  run_id TEXT NOT NULL,
@@ -169,9 +176,12 @@ export class SqliteTeamStore implements TeamStore {
169
176
  version INTEGER NOT NULL DEFAULT 1,
170
177
  PRIMARY KEY(team_name, run_id)
171
178
  );
179
+ `);
180
+ db.exec(`
172
181
  CREATE INDEX IF NOT EXISTS idx_team_runs_status
173
182
  ON team_runs(team_name, status);
174
-
183
+ `);
184
+ db.exec(`
175
185
  CREATE TABLE IF NOT EXISTS team_outcomes (
176
186
  team_name TEXT NOT NULL,
177
187
  outcome_id TEXT NOT NULL,
@@ -182,7 +192,8 @@ export class SqliteTeamStore implements TeamStore {
182
192
  version INTEGER NOT NULL DEFAULT 1,
183
193
  PRIMARY KEY(team_name, outcome_id)
184
194
  );
185
-
195
+ `);
196
+ db.exec(`
186
197
  CREATE TABLE IF NOT EXISTS team_outcome_fragments (
187
198
  team_name TEXT NOT NULL,
188
199
  outcome_id TEXT NOT NULL,
@@ -3,7 +3,19 @@ import {
3
3
  createBashTool,
4
4
  createDefaultTools,
5
5
  createReadFilesTool,
6
+ createSkillsTool,
6
7
  } from "./definitions.js";
8
+ import { INPUT_ARG_CHAR_LIMIT } from "./schemas.js";
9
+ import type { SkillsExecutorWithMetadata } from "./types.js";
10
+
11
+ function createMockSkillsExecutor(
12
+ fn: (...args: unknown[]) => Promise<string> = async () => "ok",
13
+ configuredSkills?: SkillsExecutorWithMetadata["configuredSkills"],
14
+ ): SkillsExecutorWithMetadata {
15
+ const executor = fn as SkillsExecutorWithMetadata;
16
+ executor.configuredSkills = configuredSkills;
17
+ return executor;
18
+ }
7
19
 
8
20
  describe("default skills tool", () => {
9
21
  it("is included only when enabled with a skills executor", () => {
@@ -17,18 +29,43 @@ describe("default skills tool", () => {
17
29
 
18
30
  const toolsWithExecutor = createDefaultTools({
19
31
  executors: {
20
- skills: async () => "ok",
32
+ skills: createMockSkillsExecutor(),
21
33
  },
22
34
  enableSkills: true,
23
35
  });
24
36
  expect(toolsWithExecutor.map((tool) => tool.name)).toContain("skills");
25
37
  });
26
38
 
39
+ it("includes configured skill names in description", () => {
40
+ const executor = createMockSkillsExecutor(
41
+ async () => "ok",
42
+ [
43
+ { id: "commit", name: "commit", disabled: false },
44
+ {
45
+ id: "review-pr",
46
+ name: "review-pr",
47
+ description: "Review a PR",
48
+ disabled: false,
49
+ },
50
+ { id: "disabled-skill", name: "disabled-skill", disabled: true },
51
+ ],
52
+ );
53
+ const tool = createSkillsTool(executor);
54
+ expect(tool.description).toContain("Available skills: commit, review-pr.");
55
+ expect(tool.description).not.toContain("disabled-skill");
56
+ });
57
+
58
+ it("omits skill list from description when no skills are configured", () => {
59
+ const executor = createMockSkillsExecutor(async () => "ok");
60
+ const tool = createSkillsTool(executor);
61
+ expect(tool.description).not.toContain("Available skills");
62
+ });
63
+
27
64
  it("validates and executes skill invocation input", async () => {
28
65
  const execute = vi.fn(async () => "loaded");
29
66
  const tools = createDefaultTools({
30
67
  executors: {
31
- skills: execute,
68
+ skills: createMockSkillsExecutor(execute),
32
69
  },
33
70
  enableReadFiles: false,
34
71
  enableSearch: false,
@@ -310,6 +347,48 @@ describe("default read_files tool", () => {
310
347
  }),
311
348
  );
312
349
  });
350
+
351
+ it("treats null line bounds as full-file boundaries", async () => {
352
+ const execute = vi.fn(async () => "full file");
353
+ const tool = createReadFilesTool(execute);
354
+
355
+ const result = await tool.execute(
356
+ {
357
+ files: [
358
+ {
359
+ path: "/tmp/example.ts",
360
+ start_line: null,
361
+ end_line: null,
362
+ },
363
+ ],
364
+ },
365
+ {
366
+ agentId: "agent-1",
367
+ conversationId: "conv-1",
368
+ iteration: 1,
369
+ },
370
+ );
371
+
372
+ expect(result).toEqual([
373
+ {
374
+ query: "/tmp/example.ts",
375
+ result: "full file",
376
+ success: true,
377
+ },
378
+ ]);
379
+ expect(execute).toHaveBeenCalledWith(
380
+ {
381
+ path: "/tmp/example.ts",
382
+ start_line: null,
383
+ end_line: null,
384
+ },
385
+ expect.objectContaining({
386
+ agentId: "agent-1",
387
+ conversationId: "conv-1",
388
+ iteration: 1,
389
+ }),
390
+ );
391
+ });
313
392
  });
314
393
 
315
394
  describe("zod schema conversion", () => {
@@ -329,19 +408,20 @@ describe("zod schema conversion", () => {
329
408
  "The absolute file path of a text file to read content from",
330
409
  },
331
410
  start_line: {
332
- type: "integer",
333
- description: "Optional one-based starting line number to read from",
411
+ anyOf: [{ type: "integer" }, { type: "null" }],
412
+ description:
413
+ "Optional one-based starting line number to read from; use null or omit for the start of the file",
334
414
  },
335
415
  end_line: {
336
- type: "integer",
416
+ anyOf: [{ type: "integer" }, { type: "null" }],
337
417
  description:
338
- "Optional one-based ending line number to read through",
418
+ "Optional one-based ending line number to read through; use null or omit for the end of the file",
339
419
  },
340
420
  },
341
421
  required: ["path"],
342
422
  },
343
423
  description:
344
- "Array of file read requests. Omit start_line and end_line to return the full file content; provide them to return only that inclusive one-based line range. Prefer this tool over running terminal command to get file content for better performance and reliability.",
424
+ "Array of file read requests. Omit start_line/end_line or set them to null to return the full file content boundaries; provide integers to return only that inclusive one-based line range. Prefer this tool over running terminal command to get file content for better performance and reliability.",
345
425
  });
346
426
  expect(inputSchema.required).toEqual(["files"]);
347
427
  });
@@ -349,7 +429,7 @@ describe("zod schema conversion", () => {
349
429
  it("exposes skills args as optional nullable in tool schemas", () => {
350
430
  const tools = createDefaultTools({
351
431
  executors: {
352
- skills: async () => "ok",
432
+ skills: createMockSkillsExecutor(),
353
433
  },
354
434
  enableReadFiles: false,
355
435
  enableSearch: false,
@@ -526,4 +606,53 @@ describe("default editor tool", () => {
526
606
  expect.anything(),
527
607
  );
528
608
  });
609
+
610
+ it("returns a recoverable tool error when text exceeds the soft character limit", async () => {
611
+ const execute = vi.fn(async () => "patched");
612
+ const tools = createDefaultTools({
613
+ executors: {
614
+ editor: execute,
615
+ },
616
+ enableReadFiles: false,
617
+ enableSearch: false,
618
+ enableBash: false,
619
+ enableWebFetch: false,
620
+ enableSkills: false,
621
+ enableAskQuestion: false,
622
+ enableApplyPatch: false,
623
+ enableEditor: true,
624
+ });
625
+ const editorTool = tools.find((tool) => tool.name === "editor");
626
+ expect(editorTool).toBeDefined();
627
+ if (!editorTool) {
628
+ throw new Error("Expected editor tool to be defined.");
629
+ }
630
+
631
+ const oversizedText = "x".repeat(INPUT_ARG_CHAR_LIMIT + 1);
632
+ const result = await editorTool.execute(
633
+ {
634
+ path: "/tmp/example.ts",
635
+ new_text: oversizedText,
636
+ },
637
+ {
638
+ agentId: "agent-1",
639
+ conversationId: "conv-1",
640
+ iteration: 1,
641
+ },
642
+ );
643
+
644
+ expect(result).toEqual({
645
+ query: "edit:/tmp/example.ts",
646
+ result: "",
647
+ error: expect.stringContaining("new_text was"),
648
+ success: false,
649
+ });
650
+ if (typeof result !== "object" || result == null || !("error" in result)) {
651
+ throw new Error("Expected editor tool result to include an error.");
652
+ }
653
+ expect(result.error).toContain(
654
+ `recommended limit of ${INPUT_ARG_CHAR_LIMIT}`,
655
+ );
656
+ expect(execute).not.toHaveBeenCalled();
657
+ });
529
658
  });
@@ -16,6 +16,7 @@ import {
16
16
  EditFileInputSchema,
17
17
  type FetchWebContentInput,
18
18
  FetchWebContentInputSchema,
19
+ INPUT_ARG_CHAR_LIMIT,
19
20
  type ReadFileRequest,
20
21
  type ReadFilesInput,
21
22
  ReadFilesInputSchema,
@@ -38,7 +39,7 @@ import type {
38
39
  EditorExecutor,
39
40
  FileReadExecutor,
40
41
  SearchExecutor,
41
- SkillsExecutor,
42
+ SkillsExecutorWithMetadata,
42
43
  ToolOperationResult,
43
44
  WebFetchExecutor,
44
45
  } from "./types.js";
@@ -57,6 +58,21 @@ function formatError(error: unknown): string {
57
58
  return String(error);
58
59
  }
59
60
 
61
+ function getEditorSizeError(input: EditFileInput): string | null {
62
+ if (
63
+ typeof input.old_text === "string" &&
64
+ input.old_text.length > INPUT_ARG_CHAR_LIMIT
65
+ ) {
66
+ return `Editor input too large: old_text was ${input.old_text.length} characters, exceeding the recommended limit of ${INPUT_ARG_CHAR_LIMIT}. Split the edit into smaller tool calls so later tool calls are less likely to be truncated or time out.`;
67
+ }
68
+
69
+ if (input.new_text.length > INPUT_ARG_CHAR_LIMIT) {
70
+ return `Editor input too large: new_text was ${input.new_text.length} characters, exceeding the recommended limit of ${INPUT_ARG_CHAR_LIMIT}. Split the edit into smaller tool calls so later tool calls are less likely to be truncated or time out.`;
71
+ }
72
+
73
+ return null;
74
+ }
75
+
60
76
  /**
61
77
  * Create a timeout-wrapped promise
62
78
  */
@@ -105,7 +121,7 @@ function normalizeReadFileRequests(input: unknown): ReadFileRequest[] {
105
121
 
106
122
  function formatReadFileQuery(request: ReadFileRequest): string {
107
123
  const { path, start_line, end_line } = request;
108
- if (start_line === undefined && end_line === undefined) {
124
+ if (start_line == null && end_line == null) {
109
125
  return path;
110
126
  }
111
127
  const start = start_line ?? 1;
@@ -113,63 +129,6 @@ function formatReadFileQuery(request: ReadFileRequest): string {
113
129
  return `${path}:${start}-${end}`;
114
130
  }
115
131
 
116
- const APPLY_PATCH_TOOL_DESC = `This is a custom utility that makes it more convenient to add, remove, move, or edit code in a single file. \`apply_patch\` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the \`apply_patch\` command, you should pass a message of the following structure as "input":
117
-
118
- %%bash
119
- apply_patch <<"EOF"
120
- *** Begin Patch
121
- [YOUR_PATCH]
122
- *** End Patch
123
- EOF
124
-
125
- Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
126
-
127
- *** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
128
-
129
- In a Add File section, every line of the new file (including blank/empty lines) MUST start with a \`+\` prefix. Do not include any unprefixed lines inside an Add section
130
- In a Update/Delete section, repeat the following for each snippet of code that needs to be changed:
131
- [context_before] -> See below for further instructions on context.
132
- - [old_code] -> Precede the old code with a minus sign.
133
- + [new_code] -> Precede the new, replacement code with a plus sign.
134
- [context_after] -> See below for further instructions on context.
135
-
136
- For instructions on [context_before] and [context_after]:
137
- - By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
138
- - If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
139
- @@ class BaseClass
140
- [3 lines of pre-context]
141
- - [old_code]
142
- + [new_code]
143
- [3 lines of post-context]
144
-
145
- - If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple \`@@\` statements to jump to the right context. For instance:
146
-
147
- @@ class BaseClass
148
- @@ def method():
149
- [3 lines of pre-context]
150
- - [old_code]
151
- + [new_code]
152
- [3 lines of post-context]
153
-
154
- Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
155
-
156
- %%bash
157
- apply_patch <<"EOF"
158
- *** Begin Patch
159
- *** Update File: pygorithm/searching/binary_search.py
160
- @@ class BaseClass
161
- @@ def search():
162
- - pass
163
- + raise NotImplementedError()
164
-
165
- @@ class Subclass
166
- @@ def search():
167
- - pass
168
- + raise NotImplementedError()
169
-
170
- *** End Patch
171
- EOF`;
172
-
173
132
  // =============================================================================
174
133
  // Tool Factory Functions
175
134
  // =============================================================================
@@ -404,6 +363,63 @@ export function createWebFetchTool(
404
363
  });
405
364
  }
406
365
 
366
+ const APPLY_PATCH_TOOL_DESC = `This is a custom utility that makes it more convenient to add, remove, move, or edit code in a single file. \`apply_patch\` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the \`apply_patch\` command, you should pass a message of the following structure as "input":
367
+
368
+ %%bash
369
+ apply_patch <<"EOF"
370
+ *** Begin Patch
371
+ [YOUR_PATCH]
372
+ *** End Patch
373
+ EOF
374
+
375
+ Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
376
+
377
+ *** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
378
+
379
+ In a Add File section, every line of the new file (including blank/empty lines) MUST start with a \`+\` prefix. Do not include any unprefixed lines inside an Add section
380
+ In a Update/Delete section, repeat the following for each snippet of code that needs to be changed:
381
+ [context_before] -> See below for further instructions on context.
382
+ - [old_code] -> Precede the old code with a minus sign.
383
+ + [new_code] -> Precede the new, replacement code with a plus sign.
384
+ [context_after] -> See below for further instructions on context.
385
+
386
+ For instructions on [context_before] and [context_after]:
387
+ - By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
388
+ - If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
389
+ @@ class BaseClass
390
+ [3 lines of pre-context]
391
+ - [old_code]
392
+ + [new_code]
393
+ [3 lines of post-context]
394
+
395
+ - If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple \`@@\` statements to jump to the right context. For instance:
396
+
397
+ @@ class BaseClass
398
+ @@ def method():
399
+ [3 lines of pre-context]
400
+ - [old_code]
401
+ + [new_code]
402
+ [3 lines of post-context]
403
+
404
+ Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
405
+
406
+ %%bash
407
+ apply_patch <<"EOF"
408
+ *** Begin Patch
409
+ *** Update File: pygorithm/searching/binary_search.py
410
+ @@ class BaseClass
411
+ @@ def search():
412
+ - pass
413
+ + raise NotImplementedError()
414
+
415
+ @@ class Subclass
416
+ @@ def search():
417
+ - pass
418
+ + raise NotImplementedError()
419
+
420
+ *** End Patch
421
+ EOF`;
422
+
407
423
  /**
408
424
  * Create the apply_patch tool
409
425
  *
@@ -470,7 +486,9 @@ export function createEditorTool(
470
486
  description:
471
487
  "An editor for controlled filesystem edits on the text file at the provided path. " +
472
488
  "Provide `insert_line` to insert `new_text` at a specific line number. " +
473
- "Otherwise, the tool replaces `old_text` with `new_text`, or creates the file with `new_text` if it does not exist.",
489
+ "Otherwise, the tool replaces `old_text` with `new_text`, or creates the file with `new_text` if file does not exist. " +
490
+ "Use this tools for making small, precise edits to existing files or creating new files over shell commands.",
491
+
474
492
  inputSchema: zodToJsonSchema(EditFileInputSchema),
475
493
  timeoutMs,
476
494
  retryable: false, // Editing operations are stateful and should not auto-retry
@@ -478,6 +496,16 @@ export function createEditorTool(
478
496
  execute: async (input, context) => {
479
497
  const validatedInput = validateWithZod(EditFileInputSchema, input);
480
498
  const operation = validatedInput.insert_line == null ? "edit" : "insert";
499
+ const sizeError = getEditorSizeError(validatedInput);
500
+
501
+ if (sizeError) {
502
+ return {
503
+ query: `${operation}:${validatedInput.path}`,
504
+ result: "",
505
+ error: sizeError,
506
+ success: false,
507
+ };
508
+ }
481
509
 
482
510
  try {
483
511
  const result = await withTimeout(
@@ -510,21 +538,22 @@ export function createEditorTool(
510
538
  * Invokes a configured skill by name and optional arguments.
511
539
  */
512
540
  export function createSkillsTool(
513
- executor: SkillsExecutor,
541
+ executor: SkillsExecutorWithMetadata,
514
542
  config: Pick<DefaultToolsConfig, "skillsTimeoutMs"> = {},
515
543
  ): Tool<SkillsInput, string> {
516
544
  const timeoutMs = config.skillsTimeoutMs ?? 15000;
517
545
 
518
- return createTool<SkillsInput, string>({
546
+ const baseDescription =
547
+ "Execute a skill within the main conversation. " +
548
+ "When users ask you to perform tasks, check if any available skills match. " +
549
+ "When users reference a slash command, invoke it with this tool. " +
550
+ 'Input: `skill` (required) and optional `args`. Example: `skill: "pdf"`, `skill: "commit", args: "-m \\"Fix bug\\""`, `skill: "review-pr", args: "123"`, `skill: "ms-office-suite:pdf"`. ' +
551
+ "When a skill matches the user's request, invoking this tool is a blocking requirement before any other response. " +
552
+ "Never mention a skill without invoking this tool.";
553
+
554
+ const tool = createTool<SkillsInput, string>({
519
555
  name: "skills",
520
- description:
521
- "Execute a skill within the main conversation. " +
522
- "When users ask you to perform tasks, check if any available skills match. " +
523
- 'When users reference a slash command (for example "/commit" or "/review-pr"), invoke this tool. ' +
524
- 'Input: `skill` (required) and optional `args`. Example: `skill: "pdf"`, `skill: "commit", args: "-m \\"Fix bug\\""`, `skill: "review-pr", args: "123"`, `skill: "ms-office-suite:pdf"`. ' +
525
- "Available skills are listed in system-reminder messages in the conversation. " +
526
- "When a skill matches the user's request, invoking this tool is a blocking requirement before any other response. " +
527
- "Never mention a skill without invoking this tool.",
556
+ description: baseDescription,
528
557
  inputSchema: zodToJsonSchema(SkillsInputSchema),
529
558
  timeoutMs,
530
559
  retryable: false,
@@ -542,6 +571,22 @@ export function createSkillsTool(
542
571
  );
543
572
  },
544
573
  });
574
+
575
+ Object.defineProperty(tool, "description", {
576
+ get() {
577
+ const skills = executor.configuredSkills
578
+ ?.filter((s) => !s.disabled)
579
+ .map((s) => s.name);
580
+ if (skills && skills.length > 0) {
581
+ return `${baseDescription} Available skills: ${skills.join(", ")}.`;
582
+ }
583
+ return baseDescription;
584
+ },
585
+ enumerable: true,
586
+ configurable: true,
587
+ });
588
+
589
+ return tool;
545
590
  }
546
591
 
547
592
  /**
@@ -11,10 +11,10 @@ describe("default tool presets", () => {
11
11
  expect(ToolPresets.development.enableAskQuestion).toBe(true);
12
12
  expect(ToolPresets.readonly.enableAskQuestion).toBe(true);
13
13
  expect(ToolPresets.minimal.enableAskQuestion).toBe(true);
14
- expect(ToolPresets.yolo.enableAskQuestion).toBe(true);
14
+ expect(ToolPresets.yolo.enableAskQuestion).toBe(false);
15
15
  });
16
16
 
17
- it("yolo preset enables all default tools when executors exist", () => {
17
+ it("yolo preset excludes ask_question even when its executor exists", () => {
18
18
  const tools = createDefaultToolsWithPreset("yolo", {
19
19
  executors: {
20
20
  readFile: async () => "ok",
@@ -35,7 +35,6 @@ describe("default tool presets", () => {
35
35
  "fetch_web_content",
36
36
  "editor",
37
37
  "skills",
38
- "ask_question",
39
38
  ]);
40
39
  });
41
40
  });
@@ -88,7 +88,7 @@ export const ToolPresets = {
88
88
  },
89
89
 
90
90
  /**
91
- * YOLO mode (everything enabled + no approval required)
91
+ * YOLO mode (automation-focused tools + no approval required)
92
92
  * Good for trusted local automation workflows.
93
93
  */
94
94
  yolo: {
@@ -99,7 +99,7 @@ export const ToolPresets = {
99
99
  enableApplyPatch: false,
100
100
  enableEditor: true,
101
101
  enableSkills: true,
102
- enableAskQuestion: true,
102
+ enableAskQuestion: false,
103
103
  },
104
104
  } as const satisfies Record<string, DefaultToolsConfig>;
105
105
 
@@ -115,7 +115,7 @@ export type ToolPolicyPresetName = "default" | "yolo";
115
115
 
116
116
  /**
117
117
  * Build tool policies for a preset.
118
- * `yolo` guarantees all tools are enabled and auto-approved.
118
+ * `yolo` guarantees tool policies are enabled and auto-approved.
119
119
  */
120
120
  export function createToolPoliciesWithPreset(
121
121
  presetName: ToolPolicyPresetName,