@soleri/core 9.14.0 → 9.15.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 (138) hide show
  1. package/dist/brain/brain.d.ts +9 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +11 -1
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts.map +1 -1
  6. package/dist/brain/intelligence.js +24 -0
  7. package/dist/brain/intelligence.js.map +1 -1
  8. package/dist/brain/types.d.ts +1 -0
  9. package/dist/brain/types.d.ts.map +1 -1
  10. package/dist/chat/chat-session.d.ts +6 -0
  11. package/dist/chat/chat-session.d.ts.map +1 -1
  12. package/dist/chat/chat-session.js +68 -17
  13. package/dist/chat/chat-session.js.map +1 -1
  14. package/dist/curator/curator.d.ts +6 -0
  15. package/dist/curator/curator.d.ts.map +1 -1
  16. package/dist/curator/curator.js +138 -0
  17. package/dist/curator/curator.js.map +1 -1
  18. package/dist/curator/types.d.ts +10 -0
  19. package/dist/curator/types.d.ts.map +1 -1
  20. package/dist/engine/bin/soleri-engine.js +0 -0
  21. package/dist/flows/types.d.ts +16 -16
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/intake/content-classifier.d.ts +10 -4
  27. package/dist/intake/content-classifier.d.ts.map +1 -1
  28. package/dist/intake/content-classifier.js +19 -5
  29. package/dist/intake/content-classifier.js.map +1 -1
  30. package/dist/intake/text-ingester.d.ts +18 -0
  31. package/dist/intake/text-ingester.d.ts.map +1 -1
  32. package/dist/intake/text-ingester.js +37 -13
  33. package/dist/intake/text-ingester.js.map +1 -1
  34. package/dist/planning/planner.d.ts +3 -0
  35. package/dist/planning/planner.d.ts.map +1 -1
  36. package/dist/planning/planner.js +43 -4
  37. package/dist/planning/planner.js.map +1 -1
  38. package/dist/plugins/types.d.ts +2 -2
  39. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  40. package/dist/runtime/admin-setup-ops.js +59 -20
  41. package/dist/runtime/admin-setup-ops.js.map +1 -1
  42. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  43. package/dist/runtime/facades/orchestrate-facade.js +28 -1
  44. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  45. package/dist/runtime/runtime.d.ts.map +1 -1
  46. package/dist/runtime/runtime.js +16 -0
  47. package/dist/runtime/runtime.js.map +1 -1
  48. package/dist/runtime/types.d.ts +19 -0
  49. package/dist/runtime/types.d.ts.map +1 -1
  50. package/dist/skills/sync-skills.d.ts.map +1 -1
  51. package/dist/skills/sync-skills.js +9 -3
  52. package/dist/skills/sync-skills.js.map +1 -1
  53. package/dist/skills/validate-skills.d.ts +32 -0
  54. package/dist/skills/validate-skills.d.ts.map +1 -0
  55. package/dist/skills/validate-skills.js +396 -0
  56. package/dist/skills/validate-skills.js.map +1 -0
  57. package/dist/vault/default-canonical-tags.d.ts +15 -0
  58. package/dist/vault/default-canonical-tags.d.ts.map +1 -0
  59. package/dist/vault/default-canonical-tags.js +65 -0
  60. package/dist/vault/default-canonical-tags.js.map +1 -0
  61. package/dist/vault/tag-normalizer.d.ts +42 -0
  62. package/dist/vault/tag-normalizer.d.ts.map +1 -0
  63. package/dist/vault/tag-normalizer.js +157 -0
  64. package/dist/vault/tag-normalizer.js.map +1 -0
  65. package/package.json +6 -2
  66. package/src/__tests__/embeddings.test.ts +3 -3
  67. package/src/brain/brain.ts +25 -1
  68. package/src/brain/intelligence.ts +25 -0
  69. package/src/brain/types.ts +1 -0
  70. package/src/chat/chat-session.ts +75 -17
  71. package/src/chat/chat-transport.test.ts +31 -1
  72. package/src/curator/curator.ts +180 -0
  73. package/src/curator/types.ts +10 -0
  74. package/src/index.ts +7 -0
  75. package/src/intake/content-classifier.ts +22 -4
  76. package/src/intake/text-ingester.ts +61 -12
  77. package/src/planning/planner.test.ts +86 -90
  78. package/src/planning/planner.ts +48 -4
  79. package/src/runtime/admin-setup-ops.test.ts +44 -0
  80. package/src/runtime/admin-setup-ops.ts +59 -20
  81. package/src/runtime/facades/orchestrate-facade.ts +27 -1
  82. package/src/runtime/runtime.ts +18 -0
  83. package/src/runtime/types.ts +19 -0
  84. package/src/skills/sync-skills.ts +9 -3
  85. package/src/skills/validate-skills.test.ts +205 -0
  86. package/src/skills/validate-skills.ts +470 -0
  87. package/src/vault/default-canonical-tags.ts +64 -0
  88. package/src/vault/tag-normalizer.test.ts +214 -0
  89. package/src/vault/tag-normalizer.ts +188 -0
  90. package/dist/embeddings/index.d.ts +0 -5
  91. package/dist/embeddings/index.d.ts.map +0 -1
  92. package/dist/embeddings/index.js +0 -3
  93. package/dist/embeddings/index.js.map +0 -1
  94. package/dist/knowledge-packs/knowledge-packs/community/.gitkeep +0 -0
  95. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/soleri-pack.json +0 -10
  96. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/accessibility.json +0 -53
  97. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design-tokens.json +0 -26
  98. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/design.json +0 -33
  99. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/styling.json +0 -44
  100. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux-laws.json +0 -36
  101. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-craft/vault/ux.json +0 -36
  102. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/soleri-pack.json +0 -10
  103. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/architecture.json +0 -143
  104. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/commercial.json +0 -16
  105. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/communication.json +0 -33
  106. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/component.json +0 -16
  107. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/express.json +0 -34
  108. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/leadership.json +0 -33
  109. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/methodology.json +0 -33
  110. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/monorepo.json +0 -33
  111. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/other.json +0 -73
  112. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/performance.json +0 -35
  113. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/prisma.json +0 -33
  114. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/product-strategy.json +0 -42
  115. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/react.json +0 -47
  116. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/security.json +0 -34
  117. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/testing.json +0 -33
  118. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/tooling.json +0 -85
  119. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/typescript.json +0 -34
  120. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-engineering/vault/workflow.json +0 -46
  121. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/soleri-pack.json +0 -10
  122. package/dist/knowledge-packs/knowledge-packs/salvador/salvador-uipro/vault/design.json +0 -2589
  123. package/dist/knowledge-packs/knowledge-packs/starter/architecture/soleri-pack.json +0 -10
  124. package/dist/knowledge-packs/knowledge-packs/starter/architecture/vault/patterns.json +0 -137
  125. package/dist/knowledge-packs/knowledge-packs/starter/design/soleri-pack.json +0 -10
  126. package/dist/knowledge-packs/knowledge-packs/starter/design/vault/patterns.json +0 -137
  127. package/dist/knowledge-packs/knowledge-packs/starter/security/soleri-pack.json +0 -10
  128. package/dist/knowledge-packs/knowledge-packs/starter/security/vault/patterns.json +0 -137
  129. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/api-design/soleri-pack.json +0 -0
  130. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/api-design/vault/patterns.json +0 -0
  131. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/nodejs/soleri-pack.json +0 -0
  132. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/nodejs/vault/patterns.json +0 -0
  133. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/react/soleri-pack.json +0 -0
  134. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/react/vault/patterns.json +0 -0
  135. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/testing/soleri-pack.json +0 -0
  136. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/testing/vault/patterns.json +0 -0
  137. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/typescript/soleri-pack.json +0 -0
  138. /package/dist/knowledge-packs/{knowledge-packs/starter → starter}/typescript/vault/patterns.json +0 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Unit tests for validate-skills — the user-installed SKILL.md validator.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { validateSkillDocs } from './validate-skills.js';
10
+
11
+ // ── Helpers ──────────────────────────────────────────────────────────────
12
+
13
+ function createSkillsDir(): string {
14
+ return mkdtempSync(join(tmpdir(), 'soleri-validate-skills-test-'));
15
+ }
16
+
17
+ function addSkill(skillsDir: string, skillName: string, content: string): void {
18
+ const skillDir = join(skillsDir, skillName);
19
+ mkdirSync(skillDir, { recursive: true });
20
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
21
+ }
22
+
23
+ // ── Tests ────────────────────────────────────────────────────────────────
24
+
25
+ describe('validateSkillDocs', () => {
26
+ let skillsDir: string;
27
+
28
+ beforeEach(() => {
29
+ skillsDir = createSkillsDir();
30
+ });
31
+
32
+ afterEach(() => {
33
+ rmSync(skillsDir, { recursive: true, force: true });
34
+ });
35
+
36
+ it('returns valid=true and no errors when the skills directory is empty', () => {
37
+ const result = validateSkillDocs(skillsDir);
38
+ expect(result.valid).toBe(true);
39
+ expect(result.errors).toHaveLength(0);
40
+ expect(result.totalFiles).toBe(0);
41
+ expect(result.totalExamples).toBe(0);
42
+ });
43
+
44
+ it('returns valid=true for a SKILL.md with no op-call examples', () => {
45
+ addSkill(
46
+ skillsDir,
47
+ 'my-skill',
48
+ `# My Skill
49
+
50
+ This skill does something useful.
51
+
52
+ ## Usage
53
+
54
+ Just invoke it.
55
+ `,
56
+ );
57
+
58
+ const result = validateSkillDocs(skillsDir);
59
+ expect(result.valid).toBe(true);
60
+ expect(result.errors).toHaveLength(0);
61
+ expect(result.totalFiles).toBe(1);
62
+ expect(result.totalExamples).toBe(0);
63
+ });
64
+
65
+ it('returns valid=true when op-call params match the schema', () => {
66
+ addSkill(
67
+ skillsDir,
68
+ 'capture-skill',
69
+ `# Capture Skill
70
+
71
+ Captures knowledge to the vault.
72
+
73
+ \`\`\`
74
+ YOUR_AGENT_core op:capture_knowledge params: { projectPath: ".", entries: [{ type: "pattern", domain: "testing", title: "Use vitest", description: "Prefer vitest for unit tests", severity: "info" }] }
75
+ \`\`\`
76
+ `,
77
+ );
78
+
79
+ const result = validateSkillDocs(skillsDir);
80
+ expect(result.valid).toBe(true);
81
+ expect(result.errors).toHaveLength(0);
82
+ });
83
+
84
+ it('reports an error when severity has an invalid enum value', () => {
85
+ // "suggestion" is not in the capture_knowledge severity enum (valid: critical, warning, info)
86
+ addSkill(
87
+ skillsDir,
88
+ 'bad-severity-skill',
89
+ `# Bad Skill
90
+
91
+ Example with wrong severity enum:
92
+
93
+ \`\`\`
94
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
95
+ \`\`\`
96
+ `,
97
+ );
98
+
99
+ const result = validateSkillDocs(skillsDir);
100
+ expect(result.valid).toBe(false);
101
+ expect(result.errors.length).toBeGreaterThan(0);
102
+
103
+ const severityError = result.errors.find(
104
+ (e) => e.op === 'capture_knowledge' && e.message.toLowerCase().includes('invalid'),
105
+ );
106
+ expect(severityError).toBeDefined();
107
+ expect(severityError!.file).toContain('bad-severity-skill');
108
+ });
109
+
110
+ it('reports an error when scope receives an object instead of a string', () => {
111
+ // create_plan scope expects z.string() but we pass an object
112
+ addSkill(
113
+ skillsDir,
114
+ 'bad-scope-skill',
115
+ `# Bad Scope Skill
116
+
117
+ Example with wrong scope type:
118
+
119
+ \`\`\`
120
+ YOUR_AGENT_core op:create_plan params: { title: "My Plan", objective: "Do something", scope: { included: [] } }
121
+ \`\`\`
122
+ `,
123
+ );
124
+
125
+ const result = validateSkillDocs(skillsDir);
126
+ expect(result.valid).toBe(false);
127
+ expect(result.errors.length).toBeGreaterThan(0);
128
+
129
+ const scopeError = result.errors.find(
130
+ (e) => e.op === 'create_plan' && e.message.includes('scope'),
131
+ );
132
+ expect(scopeError).toBeDefined();
133
+ expect(scopeError!.message).toContain('Expected string');
134
+ });
135
+
136
+ it('returns structured error objects with required fields', () => {
137
+ addSkill(
138
+ skillsDir,
139
+ 'structured-error-skill',
140
+ `# Structured Error Skill
141
+
142
+ \`\`\`
143
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
144
+ \`\`\`
145
+ `,
146
+ );
147
+
148
+ const result = validateSkillDocs(skillsDir);
149
+
150
+ if (result.errors.length > 0) {
151
+ const err = result.errors[0];
152
+ expect(err).toHaveProperty('file');
153
+ expect(err).toHaveProperty('op');
154
+ expect(err).toHaveProperty('message');
155
+ expect(typeof err.file).toBe('string');
156
+ expect(typeof err.op).toBe('string');
157
+ expect(typeof err.message).toBe('string');
158
+ }
159
+ });
160
+
161
+ it('includes the file path and op name in each error', () => {
162
+ addSkill(
163
+ skillsDir,
164
+ 'named-skill',
165
+ `# Named Skill
166
+
167
+ \`\`\`
168
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
169
+ \`\`\`
170
+ `,
171
+ );
172
+
173
+ const result = validateSkillDocs(skillsDir);
174
+ expect(result.errors.length).toBeGreaterThan(0);
175
+
176
+ const err = result.errors[0];
177
+ expect(err.file).toContain('named-skill');
178
+ expect(err.op).toBe('capture_knowledge');
179
+ });
180
+
181
+ it('builds a non-empty schema registry', () => {
182
+ const result = validateSkillDocs(skillsDir);
183
+ expect(result.registrySize).toBeGreaterThan(50);
184
+ });
185
+
186
+ it('handles a skills directory that does not exist', () => {
187
+ const nonExistentDir = join(skillsDir, 'does-not-exist');
188
+ const result = validateSkillDocs(nonExistentDir);
189
+ expect(result.valid).toBe(true);
190
+ expect(result.totalFiles).toBe(0);
191
+ expect(result.errors).toHaveLength(0);
192
+ });
193
+
194
+ it('counts multiple skill files correctly', () => {
195
+ addSkill(
196
+ skillsDir,
197
+ 'skill-one',
198
+ `# Skill One\n\n\`\`\`\nYOUR_AGENT_core op:capture_quick params: { title: "Test", content: "Content" }\n\`\`\`\n`,
199
+ );
200
+ addSkill(skillsDir, 'skill-two', `# Skill Two\n\nNo examples here.\n`);
201
+
202
+ const result = validateSkillDocs(skillsDir);
203
+ expect(result.totalFiles).toBe(2);
204
+ });
205
+ });
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Validator for user-installed SKILL.md op-call examples.
3
+ *
4
+ * Reads all SKILL.md files from a given skills directory (e.g. ~/.claude/skills/),
5
+ * extracts inline op-call examples, and validates their params against the actual
6
+ * Zod schemas from the facade layer.
7
+ *
8
+ * Returns structured results rather than printing or exiting — the CLI layer owns I/O.
9
+ */
10
+
11
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import type { ZodType, ZodError } from 'zod';
14
+ import type { OpDefinition } from '../facades/types.js';
15
+ import type { AgentRuntime } from '../runtime/types.js';
16
+
17
+ // ── Facade factory imports ──────────────────────────────────────────────
18
+ import { createVaultFacadeOps } from '../runtime/facades/vault-facade.js';
19
+ import { createPlanFacadeOps } from '../runtime/facades/plan-facade.js';
20
+ import { createBrainFacadeOps } from '../runtime/facades/brain-facade.js';
21
+ import { createMemoryFacadeOps } from '../runtime/facades/memory-facade.js';
22
+ import { createAdminFacadeOps } from '../runtime/facades/admin-facade.js';
23
+ import { createCuratorFacadeOps } from '../runtime/facades/curator-facade.js';
24
+ import { createLoopFacadeOps } from '../runtime/facades/loop-facade.js';
25
+ import { createOrchestrateFacadeOps } from '../runtime/facades/orchestrate-facade.js';
26
+ import { createControlFacadeOps } from '../runtime/facades/control-facade.js';
27
+ import { createContextFacadeOps } from '../runtime/facades/context-facade.js';
28
+ import { createAgencyFacadeOps } from '../runtime/facades/agency-facade.js';
29
+ import { createChatFacadeOps } from '../runtime/facades/chat-facade.js';
30
+ import { createOperatorFacadeOps } from '../runtime/facades/operator-facade.js';
31
+ import { createArchiveFacadeOps } from '../runtime/facades/archive-facade.js';
32
+ import { createSyncFacadeOps } from '../runtime/facades/sync-facade.js';
33
+ import { createReviewFacadeOps } from '../runtime/facades/review-facade.js';
34
+ import { createIntakeFacadeOps } from '../runtime/facades/intake-facade.js';
35
+ import { createLinksFacadeOps } from '../runtime/facades/links-facade.js';
36
+ import { createBranchingFacadeOps } from '../runtime/facades/branching-facade.js';
37
+ import { createTierFacadeOps } from '../runtime/facades/tier-facade.js';
38
+ import { createEmbeddingFacadeOps } from '../runtime/facades/embedding-facade.js';
39
+
40
+ // ── Public types ────────────────────────────────────────────────────────
41
+
42
+ export interface SkillValidationError {
43
+ file: string;
44
+ op: string;
45
+ message: string;
46
+ line?: number;
47
+ }
48
+
49
+ export interface SkillValidationResult {
50
+ valid: boolean;
51
+ errors: SkillValidationError[];
52
+ totalFiles: number;
53
+ totalExamples: number;
54
+ registrySize: number;
55
+ }
56
+
57
+ // ── Internal types ──────────────────────────────────────────────────────
58
+
59
+ interface OpExample {
60
+ file: string;
61
+ line: number;
62
+ opName: string;
63
+ rawParams: string;
64
+ parsedParams: Record<string, unknown> | null;
65
+ parseError?: string;
66
+ }
67
+
68
+ // ── Mock runtime ────────────────────────────────────────────────────────
69
+ // Schemas are constructed during factory calls but handlers are never invoked.
70
+
71
+ function createNoopProxy(): AgentRuntime {
72
+ const handler: ProxyHandler<object> = {
73
+ get(_target, prop) {
74
+ if (prop === Symbol.toPrimitive) return () => '';
75
+ if (prop === Symbol.iterator) return undefined;
76
+ if (prop === 'then') return undefined;
77
+ if (prop === 'toString') return () => '[mock]';
78
+ if (prop === 'valueOf') return () => 0;
79
+ if (prop === 'length') return 0;
80
+ return new Proxy(function () {}, handler);
81
+ },
82
+ apply() {
83
+ return new Proxy({}, handler);
84
+ },
85
+ };
86
+ return new Proxy({}, handler) as unknown as AgentRuntime;
87
+ }
88
+
89
+ // ── Schema registry ─────────────────────────────────────────────────────
90
+
91
+ function buildSchemaRegistry(): Map<string, ZodType> {
92
+ const runtime = createNoopProxy();
93
+ const registry = new Map<string, ZodType>();
94
+
95
+ const facadeFactories: Array<(rt: AgentRuntime) => OpDefinition[]> = [
96
+ createVaultFacadeOps,
97
+ createPlanFacadeOps,
98
+ createBrainFacadeOps,
99
+ createMemoryFacadeOps,
100
+ createAdminFacadeOps,
101
+ createCuratorFacadeOps,
102
+ createLoopFacadeOps,
103
+ createOrchestrateFacadeOps,
104
+ createControlFacadeOps,
105
+ createContextFacadeOps,
106
+ createAgencyFacadeOps,
107
+ createChatFacadeOps,
108
+ createOperatorFacadeOps,
109
+ createArchiveFacadeOps,
110
+ createSyncFacadeOps,
111
+ createReviewFacadeOps,
112
+ createIntakeFacadeOps,
113
+ createLinksFacadeOps,
114
+ createBranchingFacadeOps,
115
+ createTierFacadeOps,
116
+ createEmbeddingFacadeOps,
117
+ ];
118
+
119
+ for (const factory of facadeFactories) {
120
+ try {
121
+ const ops = factory(runtime);
122
+ for (const op of ops) {
123
+ if (op.schema) {
124
+ registry.set(op.name, op.schema);
125
+ }
126
+ }
127
+ } catch {
128
+ // Some facades may fail with the mock runtime — skip them.
129
+ }
130
+ }
131
+
132
+ return registry;
133
+ }
134
+
135
+ // ── SKILL.md discovery ──────────────────────────────────────────────────
136
+ // Discovers all SKILL.md files directly inside skillsDir.
137
+ // Supports both layouts:
138
+ // - skillsDir/{name}/SKILL.md (directory layout)
139
+ // - skillsDir/{name}.md (flat file layout)
140
+
141
+ function discoverSkillFiles(skillsDir: string): string[] {
142
+ const paths: string[] = [];
143
+
144
+ if (!existsSync(skillsDir)) return paths;
145
+
146
+ let entries: string[];
147
+ try {
148
+ entries = readdirSync(skillsDir);
149
+ } catch {
150
+ return paths;
151
+ }
152
+
153
+ for (const entry of entries) {
154
+ const entryPath = join(skillsDir, entry);
155
+ try {
156
+ const stat = statSync(entryPath);
157
+ if (stat.isDirectory()) {
158
+ const skillMd = join(entryPath, 'SKILL.md');
159
+ if (existsSync(skillMd)) {
160
+ paths.push(skillMd);
161
+ }
162
+ } else if (entry.endsWith('.md')) {
163
+ paths.push(entryPath);
164
+ }
165
+ } catch {
166
+ // Skip unreadable entries
167
+ }
168
+ }
169
+
170
+ return paths;
171
+ }
172
+
173
+ // ── SKILL.md parser ─────────────────────────────────────────────────────
174
+
175
+ function extractOpExamples(filePath: string): OpExample[] {
176
+ let content: string;
177
+ try {
178
+ content = readFileSync(filePath, 'utf-8');
179
+ } catch {
180
+ return [];
181
+ }
182
+
183
+ const lines = content.split('\n');
184
+ const examples: OpExample[] = [];
185
+
186
+ let inCodeBlock = false;
187
+ let codeBlockStart = -1;
188
+ let codeBlockLines: string[] = [];
189
+
190
+ for (let i = 0; i < lines.length; i++) {
191
+ const line = lines[i];
192
+ if (line.trimStart().startsWith('```')) {
193
+ if (!inCodeBlock) {
194
+ inCodeBlock = true;
195
+ codeBlockStart = i + 1;
196
+ codeBlockLines = [];
197
+ } else {
198
+ extractFromCodeBlock(filePath, codeBlockStart, codeBlockLines, examples);
199
+ inCodeBlock = false;
200
+ codeBlockLines = [];
201
+ }
202
+ continue;
203
+ }
204
+ if (inCodeBlock) {
205
+ codeBlockLines.push(line);
206
+ }
207
+ }
208
+
209
+ return examples;
210
+ }
211
+
212
+ function extractFromCodeBlock(
213
+ filePath: string,
214
+ startLine: number,
215
+ blockLines: string[],
216
+ results: OpExample[],
217
+ ): void {
218
+ const opPattern = /(?:YOUR_AGENT_\w+\s+)?op:(\w+)(?:\s+params:\s*(.*))?/;
219
+
220
+ for (let i = 0; i < blockLines.length; i++) {
221
+ const line = blockLines[i];
222
+ const match = line.match(opPattern);
223
+ if (!match) continue;
224
+
225
+ const opName = match[1];
226
+ const lineNum = startLine + i + 1;
227
+
228
+ let rawParams = '';
229
+
230
+ if (match[2]) {
231
+ rawParams = match[2].trim();
232
+ } else if (i + 1 < blockLines.length && blockLines[i + 1].trim().startsWith('params:')) {
233
+ const paramsLine = blockLines[i + 1].trim();
234
+ const paramsMatch = paramsLine.match(/^params:\s*(.*)/);
235
+ if (paramsMatch) {
236
+ rawParams = paramsMatch[1].trim();
237
+ }
238
+ }
239
+
240
+ if (rawParams) {
241
+ const fullParams = collectMultiLineParams(blockLines, i, rawParams);
242
+ const { parsed, error } = parseLooseJson(fullParams);
243
+
244
+ results.push({
245
+ file: filePath,
246
+ line: lineNum,
247
+ opName,
248
+ rawParams: fullParams,
249
+ parsedParams: parsed,
250
+ parseError: error,
251
+ });
252
+ }
253
+ }
254
+ }
255
+
256
+ function collectMultiLineParams(blockLines: string[], opLineIdx: number, initial: string): string {
257
+ if (isBalanced(initial)) return initial;
258
+
259
+ let startIdx = opLineIdx + 1;
260
+ if (!blockLines[opLineIdx].includes('params:') && startIdx < blockLines.length) {
261
+ if (blockLines[startIdx].trim().startsWith('params:')) {
262
+ startIdx++;
263
+ }
264
+ }
265
+
266
+ let result = initial;
267
+ for (let j = startIdx; j < blockLines.length; j++) {
268
+ const nextLine = blockLines[j].trim();
269
+ if (!nextLine) continue;
270
+ if (nextLine.match(/(?:YOUR_AGENT_\w+\s+)?op:\w+/)) break;
271
+ result += '\n' + nextLine;
272
+ if (isBalanced(result)) break;
273
+ }
274
+
275
+ return result;
276
+ }
277
+
278
+ function isBalanced(s: string): boolean {
279
+ let depth = 0;
280
+ for (const ch of s) {
281
+ if (ch === '{' || ch === '[') depth++;
282
+ if (ch === '}' || ch === ']') depth--;
283
+ if (depth < 0) return false;
284
+ }
285
+ return depth === 0;
286
+ }
287
+
288
+ function parseLooseJson(raw: string): {
289
+ parsed: Record<string, unknown> | null;
290
+ error?: string;
291
+ } {
292
+ if (!raw.trim()) return { parsed: null };
293
+
294
+ try {
295
+ const normalized = raw
296
+ .replace(/"<([^">]+)>"/g, '"$1"')
297
+ .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":')
298
+ .replace(/'/g, '"')
299
+ .replace(/,\s*([}\]])/g, '$1')
300
+ .replace(/\.\.\./g, '')
301
+ .replace(/\["?<[^>]+>"?\]/g, '["placeholder"]');
302
+
303
+ const result = JSON.parse(normalized);
304
+ return { parsed: result };
305
+ } catch (e) {
306
+ try {
307
+ const result = extractFlatParams(raw);
308
+ if (result && Object.keys(result).length > 0) {
309
+ return { parsed: result };
310
+ }
311
+ } catch {
312
+ // fall through
313
+ }
314
+ return {
315
+ parsed: null,
316
+ error: `Cannot parse params: ${(e as Error).message}`,
317
+ };
318
+ }
319
+ }
320
+
321
+ function extractFlatParams(raw: string): Record<string, unknown> | null {
322
+ const result: Record<string, unknown> = {};
323
+ const kvPattern = /(\w+)\s*:\s*(?:"([^"]*)"|\[([^\]]*)\]|(\{[^}]*\})|(\w+))/g;
324
+ let match;
325
+ let found = false;
326
+
327
+ while ((match = kvPattern.exec(raw)) !== null) {
328
+ found = true;
329
+ const key = match[1];
330
+ if (match[2] !== undefined) {
331
+ result[key] = match[2].replace(/<[^>]+>/g, 'placeholder');
332
+ } else if (match[3] !== undefined) {
333
+ result[key] = ['placeholder'];
334
+ } else if (match[4] !== undefined) {
335
+ result[key] = {};
336
+ } else if (match[5] !== undefined) {
337
+ const val = match[5];
338
+ if (val === 'true') result[key] = true;
339
+ else if (val === 'false') result[key] = false;
340
+ else if (/^\d+$/.test(val)) result[key] = parseInt(val, 10);
341
+ else result[key] = val;
342
+ }
343
+ }
344
+
345
+ return found ? result : null;
346
+ }
347
+
348
+ // ── Validation ──────────────────────────────────────────────────────────
349
+
350
+ function isPlaceholder(value: unknown): boolean {
351
+ if (typeof value !== 'string') return false;
352
+ if (value.includes('|')) return true;
353
+ if (/^<.*>$/.test(value)) return true;
354
+ if (/^(placeholder|example|value|name|id|title|description|type|domain)$/i.test(value))
355
+ return true;
356
+ if (
357
+ /^[\w]+-[\w]+$/.test(value) &&
358
+ /\b(correct|your|my|the|this|some|new|old|entry|item|current)\b/i.test(value)
359
+ )
360
+ return true;
361
+ return false;
362
+ }
363
+
364
+ function isPlaceholderIssue(
365
+ issue: { code: string; path: (string | number)[]; received?: unknown; message: string },
366
+ params: Record<string, unknown>,
367
+ ): boolean {
368
+ if (issue.code === 'invalid_enum_value') {
369
+ const received = issue.received ?? getNestedValue(params, issue.path);
370
+ if (isPlaceholder(received)) return true;
371
+ }
372
+ return false;
373
+ }
374
+
375
+ function getNestedValue(obj: Record<string, unknown>, path: (string | number)[]): unknown {
376
+ let current: unknown = obj;
377
+ for (const key of path) {
378
+ if (current === null || current === undefined || typeof current !== 'object') return undefined;
379
+ current = (current as Record<string | number, unknown>)[key];
380
+ }
381
+ return current;
382
+ }
383
+
384
+ function validateExamples(
385
+ examples: OpExample[],
386
+ registry: Map<string, ZodType>,
387
+ ): SkillValidationError[] {
388
+ const errors: SkillValidationError[] = [];
389
+
390
+ for (const ex of examples) {
391
+ if (ex.parseError) continue;
392
+ if (!ex.parsedParams) continue;
393
+
394
+ const schema = registry.get(ex.opName);
395
+ if (!schema) {
396
+ errors.push({
397
+ file: ex.file,
398
+ op: ex.opName,
399
+ line: ex.line,
400
+ message: `unknown op — not found in any facade schema registry`,
401
+ });
402
+ continue;
403
+ }
404
+
405
+ const result = (
406
+ schema as {
407
+ safeParse: (p: unknown) => { success: boolean; error?: ZodError };
408
+ }
409
+ ).safeParse(ex.parsedParams);
410
+
411
+ if (!result.success && result.error) {
412
+ for (const issue of result.error.issues) {
413
+ if (
414
+ isPlaceholderIssue(
415
+ issue as {
416
+ code: string;
417
+ path: (string | number)[];
418
+ received?: unknown;
419
+ message: string;
420
+ },
421
+ ex.parsedParams,
422
+ )
423
+ ) {
424
+ continue;
425
+ }
426
+ const path = issue.path.join('.');
427
+ errors.push({
428
+ file: ex.file,
429
+ op: ex.opName,
430
+ line: ex.line,
431
+ message: `${path ? path + ': ' : ''}${issue.message}`,
432
+ });
433
+ }
434
+ }
435
+ }
436
+
437
+ return errors;
438
+ }
439
+
440
+ // ── Public API ──────────────────────────────────────────────────────────
441
+
442
+ /**
443
+ * Validate all SKILL.md files found in `skillsDir`.
444
+ *
445
+ * `skillsDir` is the directory that contains skill subdirectories, e.g. ~/.claude/skills/.
446
+ * Each subdirectory is expected to have a SKILL.md file.
447
+ *
448
+ * @returns Structured result with errors, counts, and whether all examples are valid.
449
+ */
450
+ export function validateSkillDocs(skillsDir: string): SkillValidationResult {
451
+ const registry = buildSchemaRegistry();
452
+ const skillFiles = discoverSkillFiles(skillsDir);
453
+ let totalExamples = 0;
454
+ const allErrors: SkillValidationError[] = [];
455
+
456
+ for (const file of skillFiles) {
457
+ const examples = extractOpExamples(file);
458
+ totalExamples += examples.length;
459
+ const errors = validateExamples(examples, registry);
460
+ allErrors.push(...errors);
461
+ }
462
+
463
+ return {
464
+ valid: allErrors.length === 0,
465
+ errors: allErrors,
466
+ totalFiles: skillFiles.length,
467
+ totalExamples,
468
+ registrySize: registry.size,
469
+ };
470
+ }