morpheus-cli 0.9.5 → 0.9.7

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 (75) hide show
  1. package/README.md +63 -43
  2. package/dist/channels/discord.js +71 -21
  3. package/dist/channels/telegram.js +73 -19
  4. package/dist/cli/commands/restart.js +15 -0
  5. package/dist/cli/commands/start.js +18 -0
  6. package/dist/config/manager.js +61 -0
  7. package/dist/config/paths.js +1 -0
  8. package/dist/config/schemas.js +11 -3
  9. package/dist/http/api.js +3 -0
  10. package/dist/http/routers/link.js +239 -0
  11. package/dist/http/routers/skills.js +1 -8
  12. package/dist/runtime/apoc.js +1 -1
  13. package/dist/runtime/audit/repository.js +1 -1
  14. package/dist/runtime/link-chunker.js +214 -0
  15. package/dist/runtime/link-repository.js +301 -0
  16. package/dist/runtime/link-search.js +298 -0
  17. package/dist/runtime/link-worker.js +284 -0
  18. package/dist/runtime/link.js +295 -0
  19. package/dist/runtime/memory/sati/service.js +1 -1
  20. package/dist/runtime/memory/sqlite.js +52 -0
  21. package/dist/runtime/neo.js +1 -1
  22. package/dist/runtime/oracle.js +81 -44
  23. package/dist/runtime/scaffold.js +4 -17
  24. package/dist/runtime/skills/__tests__/loader.test.js +7 -10
  25. package/dist/runtime/skills/__tests__/registry.test.js +2 -18
  26. package/dist/runtime/skills/__tests__/tool.test.js +55 -224
  27. package/dist/runtime/skills/index.js +1 -2
  28. package/dist/runtime/skills/loader.js +0 -2
  29. package/dist/runtime/skills/registry.js +8 -20
  30. package/dist/runtime/skills/schema.js +0 -4
  31. package/dist/runtime/skills/tool.js +42 -209
  32. package/dist/runtime/smiths/delegator.js +1 -1
  33. package/dist/runtime/smiths/registry.js +1 -1
  34. package/dist/runtime/tasks/worker.js +12 -44
  35. package/dist/runtime/trinity.js +1 -1
  36. package/dist/types/config.js +14 -0
  37. package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
  38. package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
  39. package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
  40. package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
  41. package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
  42. package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
  43. package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
  44. package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
  45. package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
  46. package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
  47. package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
  48. package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
  49. package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
  50. package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
  51. package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
  52. package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
  53. package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
  54. package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
  55. package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
  56. package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
  57. package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
  58. package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
  59. package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
  60. package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
  61. package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
  62. package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
  63. package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
  64. package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
  65. package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
  66. package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/ui/sw.js +1 -1
  69. package/package.json +3 -1
  70. package/dist/runtime/__tests__/keymaker.test.js +0 -148
  71. package/dist/runtime/keymaker.js +0 -157
  72. package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
  73. package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
  74. package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
  75. package/dist/ui/assets/index-D4fzIKy1.css +0 -1
@@ -22,7 +22,6 @@ This folder contains custom skills for Morpheus.
22
22
  ---
23
23
  name: my-skill
24
24
  description: What this skill does (max 500 chars)
25
- execution_mode: sync
26
25
  version: 1.0.0
27
26
  author: your-name
28
27
  tags:
@@ -33,7 +32,7 @@ This folder contains custom skills for Morpheus.
33
32
 
34
33
  # My Skill
35
34
 
36
- Instructions for Keymaker to follow when executing this skill.
35
+ Instructions for Oracle to follow when handling this skill.
37
36
 
38
37
  ## Steps
39
38
  1. First step
@@ -43,23 +42,11 @@ This folder contains custom skills for Morpheus.
43
42
  How to format the result.
44
43
  \`\`\`
45
44
 
46
- ## Execution Modes
47
-
48
- | Mode | Tool | Description |
49
- |------|------|-------------|
50
- | sync | skill_execute | Result returned immediately (default) |
51
- | async | skill_delegate | Runs in background, notifies when done |
52
-
53
- **sync** (default): Best for quick tasks like code review, analysis.
54
- **async**: Best for long-running tasks like builds, deployments.
55
-
56
45
  ## How It Works
57
46
 
58
47
  - Oracle lists available skills in its system prompt
59
- - When a request matches a sync skill, Oracle calls \`skill_execute\`
60
- - When a request matches an async skill, Oracle calls \`skill_delegate\`
61
- - Keymaker has access to ALL tools (filesystem, shell, git, MCP, databases)
62
- - Keymaker follows SKILL.md instructions to complete the task
48
+ - When a request matches a skill, Oracle calls \`load_skill\` to load its instructions
49
+ - Oracle then follows the skill instructions using its existing tools (DevKit, MCP, databases, etc.)
63
50
 
64
51
  ## Frontmatter Schema
65
52
 
@@ -67,7 +54,6 @@ This folder contains custom skills for Morpheus.
67
54
  |-------|----------|---------|-------------|
68
55
  | name | Yes | - | Unique identifier (a-z, 0-9, hyphens) |
69
56
  | description | Yes | - | Short description (max 500 chars) |
70
- | execution_mode | No | sync | sync or async |
71
57
  | version | No | - | Semver (e.g., 1.0.0) |
72
58
  | author | No | - | Your name |
73
59
  | enabled | No | true | true/false |
@@ -85,6 +71,7 @@ export async function scaffold() {
85
71
  fs.ensureDir(PATHS.cache),
86
72
  fs.ensureDir(PATHS.commands),
87
73
  fs.ensureDir(PATHS.skills),
74
+ fs.ensureDir(PATHS.docs),
88
75
  ]);
89
76
  // Migrate config.yaml -> zaion.yaml if needed
90
77
  await migrateConfigFile();
@@ -53,7 +53,6 @@ describe('SkillLoader', () => {
53
53
  version: '1.0.0',
54
54
  author: 'Test Author',
55
55
  enabled: true,
56
- execution_mode: 'sync',
57
56
  tags: ['test', 'unit'],
58
57
  examples: ['do something', 'do another thing'],
59
58
  }, '# Test Skill\n\nInstructions here.');
@@ -66,7 +65,6 @@ describe('SkillLoader', () => {
66
65
  expect(skill.version).toBe('1.0.0');
67
66
  expect(skill.author).toBe('Test Author');
68
67
  expect(skill.enabled).toBe(true);
69
- expect(skill.execution_mode).toBe('sync');
70
68
  expect(skill.tags).toEqual(['test', 'unit']);
71
69
  expect(skill.examples).toEqual(['do something', 'do another thing']);
72
70
  expect(skill.content).toBe('# Test Skill\n\nInstructions here.');
@@ -84,7 +82,6 @@ describe('SkillLoader', () => {
84
82
  const skill = result.skills[0];
85
83
  expect(skill.name).toBe('minimal-skill');
86
84
  expect(skill.enabled).toBe(true); // default
87
- expect(skill.execution_mode).toBe('sync'); // default
88
85
  });
89
86
  it('should report error for missing SKILL.md', async () => {
90
87
  const skillDir = path.join(testDir, 'no-md-skill');
@@ -152,17 +149,17 @@ describe('SkillLoader', () => {
152
149
  expect(result.skills).toHaveLength(0);
153
150
  expect(result.errors).toHaveLength(0);
154
151
  });
155
- it('should load async skill correctly', async () => {
156
- const skillDir = path.join(testDir, 'async-skill');
152
+ it('should load skill with tags correctly', async () => {
153
+ const skillDir = path.join(testDir, 'tagged-skill');
157
154
  fs.ensureDirSync(skillDir);
158
155
  createSkillMd(skillDir, {
159
- name: 'async-skill',
160
- description: 'An async skill',
161
- execution_mode: 'async',
162
- }, 'Long-running task instructions');
156
+ name: 'tagged-skill',
157
+ description: 'A tagged skill',
158
+ tags: ['ops', 'deploy'],
159
+ }, 'Tagged task instructions');
163
160
  const result = await loader.scan();
164
161
  expect(result.skills).toHaveLength(1);
165
- expect(result.skills[0].execution_mode).toBe('async');
162
+ expect(result.skills[0].tags).toEqual(['ops', 'deploy']);
166
163
  });
167
164
  });
168
165
  describe('content handling', () => {
@@ -129,13 +129,12 @@ describe('SkillRegistry', () => {
129
129
  });
130
130
  });
131
131
  describe('getSystemPromptSection()', () => {
132
- it('should generate prompt section with sync skills', async () => {
132
+ it('should generate prompt section with skills', async () => {
133
133
  const skillDir = path.join(TEST_DIR, 'prompt-skill');
134
134
  fs.ensureDirSync(skillDir);
135
135
  createSkillMd(skillDir, {
136
136
  name: 'prompt-skill',
137
137
  description: 'A skill for prompts',
138
- execution_mode: 'sync',
139
138
  examples: ['example usage'],
140
139
  }, 'Instructions for prompt skill');
141
140
  const registry = SkillRegistry.getInstance();
@@ -144,22 +143,7 @@ describe('SkillRegistry', () => {
144
143
  expect(section).toContain('Available Skills');
145
144
  expect(section).toContain('prompt-skill');
146
145
  expect(section).toContain('A skill for prompts');
147
- expect(section).toContain('skill_execute');
148
- });
149
- it('should generate prompt section with async skills', async () => {
150
- const skillDir = path.join(TEST_DIR, 'async-skill');
151
- fs.ensureDirSync(skillDir);
152
- createSkillMd(skillDir, {
153
- name: 'async-skill',
154
- description: 'An async skill',
155
- execution_mode: 'async',
156
- }, 'Instructions for async skill');
157
- const registry = SkillRegistry.getInstance();
158
- await registry.load();
159
- const section = registry.getSystemPromptSection();
160
- expect(section).toContain('Async Skills');
161
- expect(section).toContain('async-skill');
162
- expect(section).toContain('skill_delegate');
146
+ expect(section).toContain('load_skill');
163
147
  });
164
148
  it('should return empty string when no skills', async () => {
165
149
  const registry = SkillRegistry.getInstance();
@@ -1,16 +1,12 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  // Use vi.hoisted to define mocks before they're used in vi.mock calls
3
- const { mockRegistry, mockRepository, mockDisplay, mockContext, mockKeymaker } = vi.hoisted(() => ({
3
+ const { mockRegistry, mockAudit, mockContext } = vi.hoisted(() => ({
4
4
  mockRegistry: {
5
5
  get: vi.fn(),
6
6
  getEnabled: vi.fn(() => []),
7
- getContent: vi.fn(() => null),
8
7
  },
9
- mockRepository: {
10
- createTask: vi.fn(),
11
- },
12
- mockDisplay: {
13
- log: vi.fn(),
8
+ mockAudit: {
9
+ insert: vi.fn(),
14
10
  },
15
11
  mockContext: {
16
12
  get: vi.fn(() => ({
@@ -19,13 +15,6 @@ const { mockRegistry, mockRepository, mockDisplay, mockContext, mockKeymaker } =
19
15
  origin_message_id: '123',
20
16
  origin_user_id: 'user-1',
21
17
  })),
22
- findDuplicateDelegation: vi.fn(() => null),
23
- canEnqueueDelegation: vi.fn(() => true),
24
- setDelegationAck: vi.fn(),
25
- },
26
- mockKeymaker: {
27
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
28
- executeKeymakerTask: vi.fn((_skill, _obj, _ctx) => Promise.resolve('Keymaker result')),
29
18
  },
30
19
  }));
31
20
  vi.mock('../registry.js', () => ({
@@ -33,234 +22,76 @@ vi.mock('../registry.js', () => ({
33
22
  getInstance: () => mockRegistry,
34
23
  },
35
24
  }));
36
- vi.mock('../../tasks/repository.js', () => ({
37
- TaskRepository: {
38
- getInstance: () => mockRepository,
25
+ vi.mock('../../audit/repository.js', () => ({
26
+ AuditRepository: {
27
+ getInstance: () => mockAudit,
39
28
  },
40
29
  }));
41
30
  vi.mock('../../tasks/context.js', () => ({
42
31
  TaskRequestContext: mockContext,
43
32
  }));
44
- vi.mock('../../display.js', () => ({
45
- DisplayManager: {
46
- getInstance: () => mockDisplay,
47
- },
48
- }));
49
- vi.mock('../../keymaker.js', () => ({
50
- executeKeymakerTask: (skillName, objective, context) => mockKeymaker.executeKeymakerTask(skillName, objective, context),
51
- }));
52
33
  // Now import the module under test
53
- import { SkillExecuteTool, SkillDelegateTool, getSkillExecuteDescription, getSkillDelegateDescription } from '../tool.js';
54
- describe('SkillExecuteTool (sync)', () => {
34
+ import { createLoadSkillTool } from '../tool.js';
35
+ describe('load_skill tool', () => {
36
+ let loadSkillTool;
55
37
  beforeEach(() => {
56
38
  vi.clearAllMocks();
57
39
  mockRegistry.get.mockReset();
58
40
  mockRegistry.getEnabled.mockReset();
59
- mockKeymaker.executeKeymakerTask.mockReset();
60
- mockKeymaker.executeKeymakerTask.mockResolvedValue('Keymaker result');
61
41
  mockRegistry.getEnabled.mockReturnValue([]);
42
+ loadSkillTool = createLoadSkillTool();
62
43
  });
63
- describe('getSkillExecuteDescription()', () => {
64
- it('should include enabled sync skills in description', () => {
65
- mockRegistry.getEnabled.mockReturnValue([
66
- { name: 'code-review', description: 'Review code for issues', execution_mode: 'sync' },
67
- { name: 'git-ops', description: 'Git operations helper', execution_mode: 'sync' },
68
- { name: 'deploy', description: 'Deploy to prod', execution_mode: 'async' }, // should not appear
69
- ]);
70
- const description = getSkillExecuteDescription();
71
- expect(description).toContain('code-review: Review code for issues');
72
- expect(description).toContain('git-ops: Git operations helper');
73
- expect(description).not.toContain('deploy');
74
- });
75
- it('should show no sync skills message when none enabled', () => {
76
- mockRegistry.getEnabled.mockReturnValue([]);
77
- const description = getSkillExecuteDescription();
78
- expect(description).toContain('(no sync skills enabled)');
44
+ it('should return skill content for valid enabled skill', async () => {
45
+ mockRegistry.get.mockReturnValue({
46
+ name: 'test-skill',
47
+ description: 'Test',
48
+ enabled: true,
49
+ content: '# Instructions\n\nDo the thing.',
79
50
  });
51
+ const result = await loadSkillTool.invoke({ skillName: 'test-skill' });
52
+ expect(result).toContain('Loaded skill: test-skill');
53
+ expect(result).toContain('# Instructions');
54
+ expect(result).toContain('Do the thing.');
80
55
  });
81
- describe('invoke()', () => {
82
- it('should execute sync skill via Keymaker', async () => {
83
- mockRegistry.get.mockReturnValue({
84
- name: 'test-skill',
85
- description: 'Test',
86
- enabled: true,
87
- execution_mode: 'sync',
88
- content: 'Instructions here',
89
- });
90
- mockRegistry.getEnabled.mockReturnValue([{ name: 'test-skill', execution_mode: 'sync' }]);
91
- const result = await SkillExecuteTool.invoke({
92
- skillName: 'test-skill',
93
- objective: 'do the thing',
94
- });
95
- expect(mockKeymaker.executeKeymakerTask).toHaveBeenCalledWith('test-skill', 'do the thing', expect.objectContaining({
96
- origin_channel: 'telegram',
97
- session_id: 'test-session',
98
- }));
99
- expect(result).toBe('Keymaker result');
100
- });
101
- it('should return error for non-existent skill', async () => {
102
- mockRegistry.get.mockReturnValue(undefined);
103
- mockRegistry.getEnabled.mockReturnValue([
104
- { name: 'other-skill', execution_mode: 'sync' },
105
- ]);
106
- const result = await SkillExecuteTool.invoke({
107
- skillName: 'non-existent',
108
- objective: 'do something',
109
- });
110
- expect(result).toContain('Error');
111
- expect(result).toContain('not found');
112
- expect(result).toContain('other-skill');
113
- });
114
- it('should return error for async skill', async () => {
115
- mockRegistry.get.mockReturnValue({
116
- name: 'async-skill',
117
- description: 'Async only',
118
- enabled: true,
119
- execution_mode: 'async',
120
- });
121
- const result = await SkillExecuteTool.invoke({
122
- skillName: 'async-skill',
123
- objective: 'do something',
124
- });
125
- expect(result).toContain('Error');
126
- expect(result).toContain('async-only');
127
- expect(result).toContain('skill_delegate');
56
+ it('should emit skill_loaded audit event', async () => {
57
+ mockRegistry.get.mockReturnValue({
58
+ name: 'test-skill',
59
+ description: 'Test',
60
+ enabled: true,
61
+ content: 'Instructions',
128
62
  });
63
+ await loadSkillTool.invoke({ skillName: 'test-skill' });
64
+ expect(mockAudit.insert).toHaveBeenCalledWith(expect.objectContaining({
65
+ event_type: 'skill_loaded',
66
+ agent: 'oracle',
67
+ tool_name: 'test-skill',
68
+ status: 'success',
69
+ session_id: 'test-session',
70
+ }));
129
71
  });
130
- });
131
- describe('SkillDelegateTool (async)', () => {
132
- beforeEach(() => {
133
- vi.clearAllMocks();
134
- mockRegistry.get.mockReset();
135
- mockRegistry.getEnabled.mockReset();
136
- mockRepository.createTask.mockReset();
137
- mockDisplay.log.mockReset();
138
- mockContext.findDuplicateDelegation.mockReturnValue(null);
139
- mockContext.canEnqueueDelegation.mockReturnValue(true);
140
- mockRegistry.getEnabled.mockReturnValue([]);
72
+ it('should return error for non-existent skill', async () => {
73
+ mockRegistry.get.mockReturnValue(undefined);
74
+ mockRegistry.getEnabled.mockReturnValue([
75
+ { name: 'other-skill', description: 'Other' },
76
+ ]);
77
+ const result = await loadSkillTool.invoke({ skillName: 'non-existent' });
78
+ expect(result).toContain('not found');
79
+ expect(result).toContain('other-skill');
141
80
  });
142
- describe('getSkillDelegateDescription()', () => {
143
- it('should include enabled async skills in description', () => {
144
- mockRegistry.getEnabled.mockReturnValue([
145
- { name: 'deploy-staging', description: 'Deploy to staging', execution_mode: 'async' },
146
- { name: 'batch-process', description: 'Process batch jobs', execution_mode: 'async' },
147
- { name: 'code-review', description: 'Review code', execution_mode: 'sync' }, // should not appear
148
- ]);
149
- const description = getSkillDelegateDescription();
150
- expect(description).toContain('deploy-staging: Deploy to staging');
151
- expect(description).toContain('batch-process: Process batch jobs');
152
- expect(description).not.toContain('code-review');
153
- });
154
- it('should show no async skills message when none enabled', () => {
155
- mockRegistry.getEnabled.mockReturnValue([]);
156
- const description = getSkillDelegateDescription();
157
- expect(description).toContain('(no async skills enabled)');
81
+ it('should return error for disabled skill', async () => {
82
+ mockRegistry.get.mockReturnValue({
83
+ name: 'disabled-skill',
84
+ description: 'Disabled',
85
+ enabled: false,
158
86
  });
87
+ const result = await loadSkillTool.invoke({ skillName: 'disabled-skill' });
88
+ expect(result).toContain('disabled');
159
89
  });
160
- describe('invoke()', () => {
161
- it('should create task for valid async skill', async () => {
162
- mockRegistry.get.mockReturnValue({
163
- name: 'deploy-staging',
164
- description: 'Deploy',
165
- enabled: true,
166
- execution_mode: 'async',
167
- });
168
- mockRegistry.getEnabled.mockReturnValue([{ name: 'deploy-staging', execution_mode: 'async' }]);
169
- mockRepository.createTask.mockReturnValue({
170
- id: 'task-123',
171
- agent: 'keymaker',
172
- status: 'pending',
173
- });
174
- const result = await SkillDelegateTool.invoke({
175
- skillName: 'deploy-staging',
176
- objective: 'deploy to staging',
177
- });
178
- expect(mockRepository.createTask).toHaveBeenCalledWith(expect.objectContaining({
179
- agent: 'keymaker',
180
- input: 'deploy to staging',
181
- context: JSON.stringify({ skill: 'deploy-staging' }),
182
- origin_channel: 'telegram',
183
- session_id: 'test-session',
184
- }));
185
- expect(result).toContain('task-123');
186
- expect(result).toContain('queued');
187
- });
188
- it('should return error for sync skill', async () => {
189
- mockRegistry.get.mockReturnValue({
190
- name: 'sync-skill',
191
- description: 'Sync',
192
- enabled: true,
193
- execution_mode: 'sync',
194
- });
195
- const result = await SkillDelegateTool.invoke({
196
- skillName: 'sync-skill',
197
- objective: 'do something',
198
- });
199
- expect(result).toContain('Error');
200
- expect(result).toContain('sync');
201
- expect(result).toContain('skill_execute');
202
- expect(mockRepository.createTask).not.toHaveBeenCalled();
203
- });
204
- it('should return error for non-existent skill', async () => {
205
- mockRegistry.get.mockReturnValue(undefined);
206
- mockRegistry.getEnabled.mockReturnValue([
207
- { name: 'other-skill', execution_mode: 'async' },
208
- ]);
209
- const result = await SkillDelegateTool.invoke({
210
- skillName: 'non-existent',
211
- objective: 'do something',
212
- });
213
- expect(result).toContain('Error');
214
- expect(result).toContain('not found');
215
- expect(mockRepository.createTask).not.toHaveBeenCalled();
216
- });
217
- it('should return error for disabled skill', async () => {
218
- mockRegistry.get.mockReturnValue({
219
- name: 'disabled-skill',
220
- description: 'Disabled',
221
- enabled: false,
222
- execution_mode: 'async',
223
- });
224
- const result = await SkillDelegateTool.invoke({
225
- skillName: 'disabled-skill',
226
- objective: 'do something',
227
- });
228
- expect(result).toContain('Error');
229
- expect(result).toContain('disabled');
230
- expect(mockRepository.createTask).not.toHaveBeenCalled();
231
- });
232
- it('should deduplicate delegation requests', async () => {
233
- mockRegistry.get.mockReturnValue({
234
- name: 'dup-skill',
235
- enabled: true,
236
- execution_mode: 'async',
237
- });
238
- mockContext.findDuplicateDelegation.mockReturnValue({
239
- task_id: 'existing-task',
240
- agent: 'keymaker',
241
- task: 'dup-skill:objective',
242
- });
243
- const result = await SkillDelegateTool.invoke({
244
- skillName: 'dup-skill',
245
- objective: 'objective',
246
- });
247
- expect(result).toContain('existing-task');
248
- expect(result).toContain('already queued');
249
- expect(mockRepository.createTask).not.toHaveBeenCalled();
250
- });
251
- it('should block when delegation limit reached', async () => {
252
- mockRegistry.get.mockReturnValue({
253
- name: 'limit-skill',
254
- enabled: true,
255
- execution_mode: 'async',
256
- });
257
- mockContext.canEnqueueDelegation.mockReturnValue(false);
258
- const result = await SkillDelegateTool.invoke({
259
- skillName: 'limit-skill',
260
- objective: 'objective',
261
- });
262
- expect(result).toContain('limit reached');
263
- expect(mockRepository.createTask).not.toHaveBeenCalled();
264
- });
90
+ it('should show no skills available when none enabled', async () => {
91
+ mockRegistry.get.mockReturnValue(undefined);
92
+ mockRegistry.getEnabled.mockReturnValue([]);
93
+ const result = await loadSkillTool.invoke({ skillName: 'anything' });
94
+ expect(result).toContain('not found');
95
+ expect(result).toContain('none');
265
96
  });
266
97
  });
@@ -4,5 +4,4 @@
4
4
  export { SkillRegistry } from './registry.js';
5
5
  export { SkillLoader } from './loader.js';
6
6
  export { SkillMetadataSchema } from './schema.js';
7
- export { SkillExecuteTool, SkillDelegateTool, getSkillExecuteDescription, getSkillDelegateDescription, updateSkillToolDescriptions, updateSkillDelegateDescription, // backwards compat
8
- } from './tool.js';
7
+ export { createLoadSkillTool } from './tool.js';
@@ -6,7 +6,6 @@
6
6
  * ---
7
7
  * name: my-skill
8
8
  * description: What this skill does
9
- * execution_mode: sync
10
9
  * ---
11
10
  *
12
11
  * # Skill Instructions...
@@ -200,7 +199,6 @@ export class SkillLoader {
200
199
  dirName,
201
200
  content: content.trim(),
202
201
  enabled: metadata.enabled ?? true,
203
- execution_mode: metadata.execution_mode ?? 'sync',
204
202
  };
205
203
  return { skill };
206
204
  }
@@ -107,26 +107,14 @@ export class SkillRegistry {
107
107
  if (enabled.length === 0) {
108
108
  return '';
109
109
  }
110
- const syncSkills = enabled.filter((s) => s.execution_mode === 'sync');
111
- const asyncSkills = enabled.filter((s) => s.execution_mode === 'async');
112
- const lines = ['## Available Skills', ''];
113
- if (syncSkills.length > 0) {
114
- lines.push('### Sync Skills (immediate result via skill_execute)');
115
- for (const s of syncSkills) {
116
- lines.push(`- **${s.name}**: ${s.description}`);
117
- }
118
- lines.push('');
119
- }
120
- if (asyncSkills.length > 0) {
121
- lines.push('### Async Skills (background task via skill_delegate)');
122
- for (const s of asyncSkills) {
123
- lines.push(`- **${s.name}**: ${s.description}`);
124
- }
125
- lines.push('');
126
- }
127
- lines.push('Use `skill_execute(skillName, objective)` for sync skills — result returned immediately.');
128
- lines.push('Use `skill_delegate(skillName, objective)` for async skills — runs in background, notifies when done.');
129
- return lines.join('\n');
110
+ const skillList = enabled
111
+ .map(s => `- **${s.name}**: ${s.description}`)
112
+ .join('\n');
113
+ return `## Available Skills
114
+
115
+ ${skillList}
116
+
117
+ Use the \`load_skill\` tool when you need detailed instructions for handling a specific type of request.`;
130
118
  }
131
119
  /**
132
120
  * Get skill names for tool description
@@ -15,10 +15,6 @@ export const SkillMetadataSchema = z.object({
15
15
  .string()
16
16
  .min(1, 'Description is required')
17
17
  .max(500, 'Description must be at most 500 characters'),
18
- execution_mode: z
19
- .enum(['sync', 'async'])
20
- .default('sync')
21
- .describe('Execution mode: sync returns result inline, async creates background task'),
22
18
  version: z
23
19
  .string()
24
20
  .regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., 1.0.0)')