@sentry/warden 0.0.0

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 (199) hide show
  1. package/.agents/skills/find-bugs/SKILL.md +75 -0
  2. package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
  3. package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
  4. package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  5. package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  6. package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  7. package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  8. package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  9. package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  10. package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  11. package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  12. package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  13. package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  14. package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  15. package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  16. package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  17. package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  18. package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  19. package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  20. package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  21. package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  22. package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  23. package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  24. package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  25. package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  26. package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  27. package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  28. package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  29. package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  30. package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  31. package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  32. package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  33. package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  34. package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  35. package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  36. package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  37. package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  38. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  39. package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  40. package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  41. package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  42. package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  43. package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  44. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  45. package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  46. package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  47. package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  48. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  49. package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  50. package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  51. package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  52. package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  53. package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  54. package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  55. package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  56. package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  57. package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  58. package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  59. package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  60. package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  61. package/.claude/settings.json +57 -0
  62. package/.claude/settings.local.json +88 -0
  63. package/.claude/skills/agent-prompt/SKILL.md +54 -0
  64. package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
  65. package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
  66. package/.claude/skills/agent-prompt/references/context-design.md +124 -0
  67. package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
  68. package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
  69. package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
  70. package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
  71. package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
  72. package/.claude/skills/notseer/SKILL.md +131 -0
  73. package/.claude/skills/skill-writer/SKILL.md +140 -0
  74. package/.claude/skills/testing-guidelines/SKILL.md +132 -0
  75. package/.claude/skills/warden-skill/SKILL.md +250 -0
  76. package/.claude/skills/warden-skill/references/config-schema.md +133 -0
  77. package/.dex/config.toml +2 -0
  78. package/.github/workflows/ci.yml +33 -0
  79. package/.github/workflows/release.yml +54 -0
  80. package/.github/workflows/warden.yml +40 -0
  81. package/AGENTS.md +89 -0
  82. package/CONTRIBUTING.md +60 -0
  83. package/LICENSE +105 -0
  84. package/README.md +43 -0
  85. package/SPEC.md +263 -0
  86. package/action.yml +87 -0
  87. package/assets/favicon.png +0 -0
  88. package/assets/warden-icon-bw.svg +5 -0
  89. package/assets/warden-icon-purple.png +0 -0
  90. package/assets/warden-icon-purple.svg +5 -0
  91. package/docs/.claude/settings.local.json +11 -0
  92. package/docs/astro.config.mjs +43 -0
  93. package/docs/package.json +19 -0
  94. package/docs/pnpm-lock.yaml +4000 -0
  95. package/docs/public/favicon.svg +5 -0
  96. package/docs/src/components/Code.astro +141 -0
  97. package/docs/src/components/PackageManagerTabs.astro +183 -0
  98. package/docs/src/components/Terminal.astro +212 -0
  99. package/docs/src/layouts/Base.astro +380 -0
  100. package/docs/src/pages/cli.astro +167 -0
  101. package/docs/src/pages/config.astro +394 -0
  102. package/docs/src/pages/guide.astro +449 -0
  103. package/docs/src/pages/index.astro +490 -0
  104. package/docs/src/styles/global.css +551 -0
  105. package/docs/tsconfig.json +3 -0
  106. package/docs/vercel.json +5 -0
  107. package/eslint.config.js +33 -0
  108. package/package.json +73 -0
  109. package/src/action/index.ts +1 -0
  110. package/src/action/main.ts +868 -0
  111. package/src/cli/args.test.ts +477 -0
  112. package/src/cli/args.ts +415 -0
  113. package/src/cli/commands/add.ts +447 -0
  114. package/src/cli/commands/init.test.ts +136 -0
  115. package/src/cli/commands/init.ts +132 -0
  116. package/src/cli/commands/setup-app/browser.ts +38 -0
  117. package/src/cli/commands/setup-app/credentials.ts +45 -0
  118. package/src/cli/commands/setup-app/manifest.ts +48 -0
  119. package/src/cli/commands/setup-app/server.ts +172 -0
  120. package/src/cli/commands/setup-app.ts +156 -0
  121. package/src/cli/commands/sync.ts +114 -0
  122. package/src/cli/context.ts +131 -0
  123. package/src/cli/files.test.ts +155 -0
  124. package/src/cli/files.ts +89 -0
  125. package/src/cli/fix.test.ts +310 -0
  126. package/src/cli/fix.ts +387 -0
  127. package/src/cli/git.test.ts +119 -0
  128. package/src/cli/git.ts +318 -0
  129. package/src/cli/index.ts +14 -0
  130. package/src/cli/main.ts +672 -0
  131. package/src/cli/output/box.ts +235 -0
  132. package/src/cli/output/formatters.test.ts +187 -0
  133. package/src/cli/output/formatters.ts +269 -0
  134. package/src/cli/output/icons.ts +13 -0
  135. package/src/cli/output/index.ts +44 -0
  136. package/src/cli/output/ink-runner.tsx +337 -0
  137. package/src/cli/output/jsonl.test.ts +347 -0
  138. package/src/cli/output/jsonl.ts +126 -0
  139. package/src/cli/output/reporter.ts +435 -0
  140. package/src/cli/output/tasks.ts +374 -0
  141. package/src/cli/output/tty.test.ts +117 -0
  142. package/src/cli/output/tty.ts +60 -0
  143. package/src/cli/output/verbosity.test.ts +40 -0
  144. package/src/cli/output/verbosity.ts +31 -0
  145. package/src/cli/terminal.test.ts +148 -0
  146. package/src/cli/terminal.ts +301 -0
  147. package/src/config/index.ts +3 -0
  148. package/src/config/loader.test.ts +313 -0
  149. package/src/config/loader.ts +103 -0
  150. package/src/config/schema.ts +168 -0
  151. package/src/config/writer.test.ts +119 -0
  152. package/src/config/writer.ts +84 -0
  153. package/src/diff/classify.test.ts +162 -0
  154. package/src/diff/classify.ts +92 -0
  155. package/src/diff/coalesce.test.ts +208 -0
  156. package/src/diff/coalesce.ts +133 -0
  157. package/src/diff/context.test.ts +226 -0
  158. package/src/diff/context.ts +201 -0
  159. package/src/diff/index.ts +4 -0
  160. package/src/diff/parser.test.ts +212 -0
  161. package/src/diff/parser.ts +149 -0
  162. package/src/event/context.ts +132 -0
  163. package/src/event/index.ts +2 -0
  164. package/src/event/schedule-context.ts +101 -0
  165. package/src/examples/examples.integration.test.ts +66 -0
  166. package/src/examples/index.test.ts +101 -0
  167. package/src/examples/index.ts +122 -0
  168. package/src/examples/setup.ts +25 -0
  169. package/src/index.ts +115 -0
  170. package/src/output/dedup.test.ts +419 -0
  171. package/src/output/dedup.ts +607 -0
  172. package/src/output/github-checks.test.ts +300 -0
  173. package/src/output/github-checks.ts +476 -0
  174. package/src/output/github-issues.ts +329 -0
  175. package/src/output/index.ts +5 -0
  176. package/src/output/issue-renderer.ts +197 -0
  177. package/src/output/renderer.test.ts +727 -0
  178. package/src/output/renderer.ts +217 -0
  179. package/src/output/stale.test.ts +375 -0
  180. package/src/output/stale.ts +155 -0
  181. package/src/output/types.ts +34 -0
  182. package/src/sdk/index.ts +1 -0
  183. package/src/sdk/runner.test.ts +806 -0
  184. package/src/sdk/runner.ts +1232 -0
  185. package/src/skills/index.ts +36 -0
  186. package/src/skills/loader.test.ts +300 -0
  187. package/src/skills/loader.ts +423 -0
  188. package/src/skills/remote.test.ts +704 -0
  189. package/src/skills/remote.ts +604 -0
  190. package/src/triggers/matcher.test.ts +277 -0
  191. package/src/triggers/matcher.ts +152 -0
  192. package/src/types/index.ts +194 -0
  193. package/src/utils/async.ts +18 -0
  194. package/src/utils/index.test.ts +84 -0
  195. package/src/utils/index.ts +50 -0
  196. package/tsconfig.json +25 -0
  197. package/vitest.config.ts +8 -0
  198. package/vitest.integration.config.ts +11 -0
  199. package/warden.toml +19 -0
@@ -0,0 +1,36 @@
1
+ export {
2
+ clearSkillsCache,
3
+ discoverAllSkills,
4
+ loadSkillFromFile,
5
+ loadSkillFromMarkdown,
6
+ loadSkillsFromDirectory,
7
+ resolveSkillAsync,
8
+ SkillLoaderError,
9
+ } from './loader.js';
10
+
11
+ export type { DiscoveredSkill, LoadedSkill, LoadSkillsOptions, ResolveSkillOptions } from './loader.js';
12
+
13
+ export {
14
+ parseRemoteRef,
15
+ formatRemoteRef,
16
+ getSkillsCacheDir,
17
+ getRemotePath,
18
+ getStatePath,
19
+ loadState,
20
+ saveState,
21
+ getCacheTtlSeconds,
22
+ shouldRefresh,
23
+ fetchRemote,
24
+ discoverRemoteSkills,
25
+ resolveRemoteSkill,
26
+ removeRemote,
27
+ listCachedRemotes,
28
+ } from './remote.js';
29
+
30
+ export type {
31
+ ParsedRemoteRef,
32
+ RemoteEntry,
33
+ RemoteState,
34
+ FetchRemoteOptions,
35
+ DiscoveredRemoteSkill,
36
+ } from './remote.js';
@@ -0,0 +1,300 @@
1
+ import { describe, it, expect, beforeEach, afterAll } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { writeFileSync, unlinkSync, mkdtempSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import {
7
+ clearSkillsCache,
8
+ loadSkillFromFile,
9
+ loadSkillFromMarkdown,
10
+ loadSkillsFromDirectory,
11
+ resolveSkillAsync,
12
+ resolveSkillPath,
13
+ SkillLoaderError,
14
+ SKILL_DIRECTORIES,
15
+ } from './loader.js';
16
+
17
+ describe('loadSkillFromFile', () => {
18
+ it('rejects unsupported file types', async () => {
19
+ await expect(loadSkillFromFile('/path/to/skill.json')).rejects.toThrow(SkillLoaderError);
20
+ await expect(loadSkillFromFile('/path/to/skill.json')).rejects.toThrow('Unsupported skill file');
21
+ });
22
+
23
+ it('throws for missing files', async () => {
24
+ await expect(loadSkillFromFile('/nonexistent/skill.md')).rejects.toThrow(SkillLoaderError);
25
+ });
26
+ });
27
+
28
+ describe('resolveSkillAsync', () => {
29
+ it('resolves skills from conventional directories', async () => {
30
+ const repoRoot = new URL('../..', import.meta.url).pathname;
31
+ const skill = await resolveSkillAsync('testing-guidelines', repoRoot);
32
+ expect(skill.name).toBe('testing-guidelines');
33
+ expect(skill.description).toBeDefined();
34
+ });
35
+
36
+ it('throws for unknown skills', async () => {
37
+ await expect(resolveSkillAsync('nonexistent-skill')).rejects.toThrow(SkillLoaderError);
38
+ await expect(resolveSkillAsync('nonexistent-skill')).rejects.toThrow('Skill not found');
39
+ });
40
+ });
41
+
42
+ describe('skills caching', () => {
43
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
44
+
45
+ beforeEach(() => {
46
+ clearSkillsCache();
47
+ });
48
+
49
+ it('caches directory loads', async () => {
50
+ const skills1 = await loadSkillsFromDirectory(skillsDir);
51
+ expect(skills1.size).toBeGreaterThan(0);
52
+
53
+ // Second load should return cached result (same reference)
54
+ const skills2 = await loadSkillsFromDirectory(skillsDir);
55
+ expect(skills2).toBe(skills1);
56
+ });
57
+
58
+ it('clearSkillsCache clears the cache', async () => {
59
+ const skills1 = await loadSkillsFromDirectory(skillsDir);
60
+
61
+ clearSkillsCache();
62
+
63
+ const skills2 = await loadSkillsFromDirectory(skillsDir);
64
+ // After clearing, should be a new Map instance
65
+ expect(skills2).not.toBe(skills1);
66
+ });
67
+ });
68
+
69
+ describe('rootDir tracking', () => {
70
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
71
+
72
+ it('sets rootDir when loading from markdown', async () => {
73
+ const skillPath = join(skillsDir, 'testing-guidelines', 'SKILL.md');
74
+ const skill = await loadSkillFromMarkdown(skillPath);
75
+ expect(skill.rootDir).toBe(join(skillsDir, 'testing-guidelines'));
76
+ });
77
+
78
+ it('sets rootDir for skills from conventional directories', async () => {
79
+ const repoRoot = new URL('../..', import.meta.url).pathname;
80
+ const skill = await resolveSkillAsync('testing-guidelines', repoRoot);
81
+ expect(skill).toBeDefined();
82
+ expect(skill.rootDir).toContain('skills');
83
+ expect(skill.rootDir).toContain('testing-guidelines');
84
+ });
85
+ });
86
+
87
+ describe('direct path resolution', () => {
88
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
89
+
90
+ it('resolves skill from directory path with SKILL.md', async () => {
91
+ const skillDir = join(skillsDir, 'testing-guidelines');
92
+ const skill = await resolveSkillAsync(skillDir);
93
+ expect(skill.name).toBe('testing-guidelines');
94
+ expect(skill.rootDir).toBe(skillDir);
95
+ });
96
+
97
+ it('resolves skill from file path', async () => {
98
+ const skillPath = join(skillsDir, 'testing-guidelines', 'SKILL.md');
99
+ const skill = await resolveSkillAsync(skillPath);
100
+ expect(skill.name).toBe('testing-guidelines');
101
+ });
102
+
103
+ it('resolves relative path with repoRoot', async () => {
104
+ const repoRoot = new URL('../..', import.meta.url).pathname;
105
+ const skill = await resolveSkillAsync('./.claude/skills/testing-guidelines', repoRoot);
106
+ expect(skill.name).toBe('testing-guidelines');
107
+ });
108
+
109
+ it('throws for nonexistent path', async () => {
110
+ await expect(resolveSkillAsync('./nonexistent/skill')).rejects.toThrow(SkillLoaderError);
111
+ await expect(resolveSkillAsync('./nonexistent/skill')).rejects.toThrow('Skill not found at path');
112
+ });
113
+ });
114
+
115
+ describe('SKILL_DIRECTORIES', () => {
116
+ it('contains expected directories in order', () => {
117
+ expect(SKILL_DIRECTORIES).toEqual([
118
+ '.warden/skills',
119
+ '.agents/skills',
120
+ '.claude/skills',
121
+ ]);
122
+ });
123
+ });
124
+
125
+ describe('resolveSkillPath', () => {
126
+ it('expands ~ to home directory', () => {
127
+ const result = resolveSkillPath('~/code/skills/my-skill');
128
+ expect(result).toBe(join(homedir(), 'code/skills/my-skill'));
129
+ });
130
+
131
+ it('expands lone ~ to home directory', () => {
132
+ const result = resolveSkillPath('~');
133
+ expect(result).toBe(homedir());
134
+ });
135
+
136
+ it('preserves absolute paths', () => {
137
+ const absolutePath = '/Users/test/code/skills/my-skill';
138
+ const result = resolveSkillPath(absolutePath, '/some/repo');
139
+ expect(result).toBe(absolutePath);
140
+ });
141
+
142
+ it('joins relative paths with repoRoot', () => {
143
+ const result = resolveSkillPath('./skills/my-skill', '/repo/root');
144
+ expect(result).toBe('/repo/root/skills/my-skill');
145
+ });
146
+
147
+ it('returns relative path as-is when no repoRoot', () => {
148
+ const result = resolveSkillPath('./skills/my-skill');
149
+ expect(result).toBe('./skills/my-skill');
150
+ });
151
+ });
152
+
153
+ describe('resolveSkillAsync with absolute and tilde paths', () => {
154
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
155
+
156
+ it('resolves absolute path to skill directory', async () => {
157
+ const absolutePath = join(skillsDir, 'testing-guidelines');
158
+ const skill = await resolveSkillAsync(absolutePath, '/different/repo');
159
+ expect(skill.name).toBe('testing-guidelines');
160
+ });
161
+
162
+ it('resolves absolute path to skill file', async () => {
163
+ const absolutePath = join(skillsDir, 'testing-guidelines', 'SKILL.md');
164
+ const skill = await resolveSkillAsync(absolutePath, '/different/repo');
165
+ expect(skill.name).toBe('testing-guidelines');
166
+ });
167
+
168
+ it('resolves tilde path to skill directory', async () => {
169
+ // Create a path using ~ that points to the skills dir
170
+ const homeRelativePath = skillsDir.replace(homedir(), '~');
171
+ // Only run this test if the skills dir is under home
172
+ if (homeRelativePath.startsWith('~/')) {
173
+ const skill = await resolveSkillAsync(`${homeRelativePath}/testing-guidelines`, '/different/repo');
174
+ expect(skill.name).toBe('testing-guidelines');
175
+ }
176
+ });
177
+ });
178
+
179
+ describe('flat markdown skill files', () => {
180
+ const tempDir = mkdtempSync(join(tmpdir(), 'warden-test-'));
181
+ const tempSkillPath = join(tempDir, 'my-custom-skill.md');
182
+
183
+ // Create a flat .md skill file with non-SKILL.md filename
184
+ writeFileSync(
185
+ tempSkillPath,
186
+ `---
187
+ name: my-custom-skill
188
+ description: A test skill with custom filename
189
+ ---
190
+
191
+ This is the prompt content.
192
+ `
193
+ );
194
+
195
+ afterAll(() => {
196
+ try {
197
+ unlinkSync(tempSkillPath);
198
+ } catch {
199
+ // ignore cleanup errors
200
+ }
201
+ });
202
+
203
+ it('loads flat .md files with any filename (not just SKILL.md)', async () => {
204
+ const skill = await loadSkillFromFile(tempSkillPath);
205
+ expect(skill.name).toBe('my-custom-skill');
206
+ expect(skill.description).toBe('A test skill with custom filename');
207
+ expect(skill.prompt).toBe('This is the prompt content.');
208
+ });
209
+
210
+ it('loadSkillFromFile accepts .md extension', async () => {
211
+ // A flat .md file should be loaded using loadSkillFromMarkdown
212
+ // (same as SKILL.md format with frontmatter)
213
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
214
+ const skillMdPath = join(skillsDir, 'testing-guidelines', 'SKILL.md');
215
+ const skill = await loadSkillFromFile(skillMdPath);
216
+ expect(skill.name).toBe('testing-guidelines');
217
+ });
218
+
219
+ it('loadSkillsFromDirectory returns entry paths for tracking', async () => {
220
+ const skillsDir = new URL('../../.claude/skills', import.meta.url).pathname;
221
+ clearSkillsCache();
222
+ const skills = await loadSkillsFromDirectory(skillsDir);
223
+
224
+ // Each loaded skill should have an entry field matching the directory name
225
+ const skillWriter = skills.get('testing-guidelines');
226
+ expect(skillWriter).toBeDefined();
227
+ expect(skillWriter!.skill.name).toBe('testing-guidelines');
228
+ expect(skillWriter!.entry).toBe('testing-guidelines');
229
+ });
230
+
231
+ it('loadSkillsFromDirectory calls onWarning for malformed skills', async () => {
232
+ const warnings: string[] = [];
233
+ const onWarning = (message: string) => warnings.push(message);
234
+
235
+ // Create a temp directory with a malformed skill
236
+ const tempDir = join(import.meta.dirname, '.test-malformed-skills');
237
+ const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
238
+ try {
239
+ mkdirSync(tempDir, { recursive: true });
240
+ // Create a .md file with frontmatter but missing required name field
241
+ writeFileSync(
242
+ join(tempDir, 'bad-skill.md'),
243
+ `---
244
+ description: Missing name field
245
+ ---
246
+ Content here
247
+ `
248
+ );
249
+
250
+ clearSkillsCache();
251
+ await loadSkillsFromDirectory(tempDir, { onWarning });
252
+
253
+ expect(warnings.length).toBe(1);
254
+ expect(warnings[0]).toContain('bad-skill.md');
255
+ expect(warnings[0]).toContain("missing 'name'");
256
+ } finally {
257
+ rmSync(tempDir, { recursive: true, force: true });
258
+ }
259
+ });
260
+
261
+ it('warns when invalid tool names are filtered from allowed-tools', async () => {
262
+ const warnings: string[] = [];
263
+ const onWarning = (message: string) => warnings.push(message);
264
+
265
+ // Create a temp directory with a skill containing invalid tool names
266
+ const tempDir = join(import.meta.dirname, '.test-invalid-tools');
267
+ const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
268
+ try {
269
+ mkdirSync(tempDir, { recursive: true });
270
+ // Create a skill with a mix of valid and invalid tool names
271
+ writeFileSync(
272
+ join(tempDir, 'test-skill.md'),
273
+ `---
274
+ name: test-skill
275
+ description: A test skill with invalid tools
276
+ allowed-tools: Read InvalidTool Grep FakeTool
277
+ ---
278
+ Test prompt content.
279
+ `
280
+ );
281
+
282
+ clearSkillsCache();
283
+ const skills = await loadSkillsFromDirectory(tempDir, { onWarning });
284
+
285
+ // Skill should still load with only valid tools
286
+ const skill = skills.get('test-skill');
287
+ expect(skill).toBeDefined();
288
+ expect(skill!.skill.tools?.allowed).toEqual(['Read', 'Grep']);
289
+
290
+ // Should have warnings for each invalid tool
291
+ expect(warnings.length).toBe(2);
292
+ expect(warnings[0]).toContain("Invalid tool name 'InvalidTool'");
293
+ expect(warnings[0]).toContain('ignored');
294
+ expect(warnings[0]).toContain('Valid tools:');
295
+ expect(warnings[1]).toContain("Invalid tool name 'FakeTool'");
296
+ } finally {
297
+ rmSync(tempDir, { recursive: true, force: true });
298
+ }
299
+ });
300
+ });