pi-harness-runtime 1.1.95 → 1.1.97

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 (226) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/harness/loop-runtime.ts +3 -0
  3. package/index.ts +130 -29
  4. package/package.json +2 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/evaluation-engine/package.json +1 -1
  29. package/packages/evaluation-runner/package.json +1 -1
  30. package/packages/event-bus/package.json +1 -1
  31. package/packages/event-store/package.json +1 -1
  32. package/packages/experience-replay/package.json +1 -1
  33. package/packages/feedback-collector/package.json +1 -1
  34. package/packages/file-copy-helper/package.json +1 -1
  35. package/packages/framework-detector/package.json +1 -1
  36. package/packages/framework-plugin-sdk/package.json +1 -1
  37. package/packages/frappe-plugin/package.json +1 -1
  38. package/packages/generic-web-plugin/package.json +1 -1
  39. package/packages/health-monitor/package.json +1 -1
  40. package/packages/intent-analyzer/package.json +1 -1
  41. package/packages/jev-judge/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/package.json +1 -1
  52. package/packages/observability/package.json +1 -1
  53. package/packages/okf-indexer/package.json +1 -1
  54. package/packages/performance-optimizer/package.json +1 -1
  55. package/packages/privilege-broker/package.json +1 -1
  56. package/packages/project-analyzer/package.json +1 -1
  57. package/packages/project-bootstrap/package.json +1 -1
  58. package/packages/projection-engine/package.json +1 -1
  59. package/packages/prompt-compiler/package.json +1 -1
  60. package/packages/prompt-versioning/package.json +1 -1
  61. package/packages/provider-adapter-sdk/package.json +1 -1
  62. package/packages/provider-router/package.json +1 -1
  63. package/packages/provider-selector/package.json +1 -1
  64. package/packages/providers/package.json +1 -1
  65. package/packages/qdrant-skills/dist/client.d.ts +51 -0
  66. package/packages/qdrant-skills/dist/client.d.ts.map +1 -0
  67. package/packages/qdrant-skills/dist/client.js +105 -0
  68. package/packages/qdrant-skills/dist/client.js.map +1 -0
  69. package/packages/qdrant-skills/dist/embedder.d.ts +39 -0
  70. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -0
  71. package/packages/qdrant-skills/dist/embedder.js +101 -0
  72. package/packages/qdrant-skills/dist/embedder.js.map +1 -0
  73. package/packages/qdrant-skills/dist/index.d.ts +25 -0
  74. package/packages/qdrant-skills/dist/index.d.ts.map +1 -0
  75. package/packages/qdrant-skills/dist/index.js +25 -0
  76. package/packages/qdrant-skills/dist/index.js.map +1 -0
  77. package/packages/qdrant-skills/package.json +27 -0
  78. package/packages/qdrant-skills/src/client.ts +178 -0
  79. package/packages/qdrant-skills/src/embedder.ts +137 -0
  80. package/packages/qdrant-skills/src/index.ts +42 -0
  81. package/packages/quota-manager/package.json +1 -1
  82. package/packages/rate-limiter/package.json +1 -1
  83. package/packages/react-vite-plugin/package.json +1 -1
  84. package/packages/release-manager/package.json +1 -1
  85. package/packages/requirement-compiler/package.json +1 -1
  86. package/packages/runtime/package.json +1 -1
  87. package/packages/scheduler/package.json +1 -1
  88. package/packages/scheduler-adapter/package.json +1 -1
  89. package/packages/session/package.json +1 -1
  90. package/packages/session-api/package.json +1 -1
  91. package/packages/session-export/package.json +1 -1
  92. package/packages/shared-context/package.json +1 -1
  93. package/packages/skill-mcp-client/package.json +1 -1
  94. package/packages/skill-registry/package.json +1 -1
  95. package/packages/skills/dist/index.d.ts +40 -0
  96. package/packages/skills/dist/index.d.ts.map +1 -0
  97. package/packages/skills/dist/index.js +84 -0
  98. package/packages/skills/dist/index.js.map +1 -0
  99. package/packages/skills/dist/linter.d.ts +38 -0
  100. package/packages/skills/dist/linter.d.ts.map +1 -0
  101. package/packages/skills/dist/linter.js +323 -0
  102. package/packages/skills/dist/linter.js.map +1 -0
  103. package/packages/skills/dist/loader.d.ts +74 -0
  104. package/packages/skills/dist/loader.d.ts.map +1 -0
  105. package/packages/skills/dist/loader.js +280 -0
  106. package/packages/skills/dist/loader.js.map +1 -0
  107. package/packages/skills/dist/parser.d.ts +32 -0
  108. package/packages/skills/dist/parser.d.ts.map +1 -0
  109. package/packages/skills/dist/parser.js +459 -0
  110. package/packages/skills/dist/parser.js.map +1 -0
  111. package/packages/skills/dist/scanner.d.ts +33 -0
  112. package/packages/skills/dist/scanner.d.ts.map +1 -0
  113. package/packages/skills/dist/scanner.js +257 -0
  114. package/packages/skills/dist/scanner.js.map +1 -0
  115. package/packages/skills/dist/skill-commands.d.ts +33 -0
  116. package/packages/skills/dist/skill-commands.d.ts.map +1 -0
  117. package/packages/skills/dist/skill-commands.js +239 -0
  118. package/packages/skills/dist/skill-commands.js.map +1 -0
  119. package/packages/skills/dist/skill-tool.d.ts +120 -0
  120. package/packages/skills/dist/skill-tool.d.ts.map +1 -0
  121. package/packages/skills/dist/skill-tool.js +239 -0
  122. package/packages/skills/dist/skill-tool.js.map +1 -0
  123. package/packages/skills/dist/skill-versioning.d.ts +123 -0
  124. package/packages/skills/dist/skill-versioning.d.ts.map +1 -0
  125. package/packages/skills/dist/skill-versioning.js +240 -0
  126. package/packages/skills/dist/skill-versioning.js.map +1 -0
  127. package/packages/skills/dist/types.d.ts +226 -0
  128. package/packages/skills/dist/types.d.ts.map +1 -0
  129. package/packages/skills/dist/types.js +21 -0
  130. package/packages/skills/dist/types.js.map +1 -0
  131. package/packages/skills/package.json +46 -0
  132. package/packages/skills/src/index.ts +95 -0
  133. package/packages/skills/src/linter.ts +371 -0
  134. package/packages/skills/src/loader.ts +335 -0
  135. package/packages/skills/src/parser.ts +548 -0
  136. package/packages/skills/src/scanner.ts +322 -0
  137. package/packages/skills/src/skill-commands.ts +318 -0
  138. package/packages/skills/src/skill-tool.ts +324 -0
  139. package/packages/skills/src/skill-versioning.ts +345 -0
  140. package/packages/skills/src/types.ts +259 -0
  141. package/packages/skills/test/init-skills.test.ts +118 -0
  142. package/packages/sprint-planner/package.json +1 -1
  143. package/packages/subscription-engine/package.json +1 -1
  144. package/packages/task-compiler/package.json +1 -1
  145. package/packages/tencentdb-memory/package.json +1 -1
  146. package/packages/tencentdb-sync/package.json +1 -1
  147. package/packages/test-data-generator/package.json +1 -1
  148. package/packages/test-generator/package.json +1 -1
  149. package/packages/todo-bd-sync/package.json +1 -1
  150. package/packages/token-estimation/package.json +1 -1
  151. package/packages/token-optimizer/package.json +1 -1
  152. package/packages/trajectory/dist/src/curator.d.ts +189 -0
  153. package/packages/trajectory/dist/src/curator.d.ts.map +1 -0
  154. package/packages/trajectory/dist/src/curator.js +340 -0
  155. package/packages/trajectory/dist/src/curator.js.map +1 -0
  156. package/packages/trajectory/dist/src/journey-renderer.d.ts +53 -0
  157. package/packages/trajectory/dist/src/journey-renderer.d.ts.map +1 -0
  158. package/packages/trajectory/dist/src/journey-renderer.js +324 -0
  159. package/packages/trajectory/dist/src/journey-renderer.js.map +1 -0
  160. package/packages/trajectory/dist/src/learning-graph.d.ts +159 -0
  161. package/packages/trajectory/dist/src/learning-graph.d.ts.map +1 -0
  162. package/packages/trajectory/dist/src/learning-graph.js +258 -0
  163. package/packages/trajectory/dist/src/learning-graph.js.map +1 -0
  164. package/packages/trajectory/dist/src/memory-store.d.ts +146 -0
  165. package/packages/trajectory/dist/src/memory-store.d.ts.map +1 -0
  166. package/packages/trajectory/dist/src/memory-store.js +472 -0
  167. package/packages/trajectory/dist/src/memory-store.js.map +1 -0
  168. package/packages/trajectory/dist/src/review-fork.d.ts +164 -0
  169. package/packages/trajectory/dist/src/review-fork.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/review-fork.js +332 -0
  171. package/packages/trajectory/dist/src/review-fork.js.map +1 -0
  172. package/packages/trajectory/dist/src/write-approval.d.ts +136 -0
  173. package/packages/trajectory/dist/src/write-approval.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/write-approval.js +281 -0
  175. package/packages/trajectory/dist/src/write-approval.js.map +1 -0
  176. package/packages/trajectory/package.json +1 -1
  177. package/packages/trajectory/src/curator.ts +485 -0
  178. package/packages/trajectory/src/journey-renderer.ts +399 -0
  179. package/packages/trajectory/src/learning-graph.ts +363 -0
  180. package/packages/trajectory/src/memory-store.ts +578 -0
  181. package/packages/trajectory/src/review-fork.ts +450 -0
  182. package/packages/trajectory/src/write-approval.ts +372 -0
  183. package/packages/tui/package.json +1 -1
  184. package/packages/types/package.json +1 -1
  185. package/packages/workflow-events/package.json +1 -1
  186. package/packages/workspace-scanner/package.json +1 -1
  187. package/packages/worktree/package.json +1 -1
  188. package/packages/write-review/dist/blackboard.d.ts +98 -0
  189. package/packages/write-review/dist/blackboard.d.ts.map +1 -0
  190. package/packages/write-review/dist/blackboard.js +289 -0
  191. package/packages/write-review/dist/blackboard.js.map +1 -0
  192. package/packages/write-review/dist/event-bus.d.ts +106 -0
  193. package/packages/write-review/dist/event-bus.d.ts.map +1 -0
  194. package/packages/write-review/dist/event-bus.js +294 -0
  195. package/packages/write-review/dist/event-bus.js.map +1 -0
  196. package/packages/write-review/dist/gate.d.ts +37 -0
  197. package/packages/write-review/dist/gate.d.ts.map +1 -0
  198. package/packages/write-review/dist/gate.js +138 -0
  199. package/packages/write-review/dist/gate.js.map +1 -0
  200. package/packages/write-review/dist/index.d.ts +22 -0
  201. package/packages/write-review/dist/index.d.ts.map +1 -0
  202. package/packages/write-review/dist/index.js +60 -0
  203. package/packages/write-review/dist/index.js.map +1 -0
  204. package/packages/write-review/dist/injection.d.ts +40 -0
  205. package/packages/write-review/dist/injection.d.ts.map +1 -0
  206. package/packages/write-review/dist/injection.js +179 -0
  207. package/packages/write-review/dist/injection.js.map +1 -0
  208. package/packages/write-review/dist/review.d.ts +54 -0
  209. package/packages/write-review/dist/review.d.ts.map +1 -0
  210. package/packages/write-review/dist/review.js +154 -0
  211. package/packages/write-review/dist/review.js.map +1 -0
  212. package/packages/write-review/dist/trigger.d.ts +39 -0
  213. package/packages/write-review/dist/trigger.d.ts.map +1 -0
  214. package/packages/write-review/dist/trigger.js +107 -0
  215. package/packages/write-review/dist/trigger.js.map +1 -0
  216. package/packages/write-review/dist/types.d.ts +78 -0
  217. package/packages/write-review/dist/types.d.ts.map +1 -0
  218. package/packages/write-review/dist/types.js +26 -0
  219. package/packages/write-review/dist/types.js.map +1 -0
  220. package/packages/write-review/package.json +1 -1
  221. package/packages/write-review/src/event-bus.ts +351 -0
  222. package/packages/write-review/src/index.ts +1 -0
  223. package/packages/write-review/src/injection.ts +25 -3
  224. package/proactive-compact.ts +51 -7
  225. package/scripts/sync-skills.ts +275 -0
  226. package/scripts/sync-to-qdrant.ts +398 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Skill Linter
3
+ *
4
+ * Hermes-style quality checks for skills.
5
+ * Ensures skills capture "lessons, not logs".
6
+ */
7
+
8
+ import type { Skill, LintResult, LintRule, LintSeverity } from "./types.js";
9
+
10
+ /**
11
+ * Hermes-style linter rules
12
+ */
13
+ export const LINTER_RULES: LintRule[] = [
14
+ // === Quality Rules ===
15
+
16
+ {
17
+ name: "incident-log-shape",
18
+ description: "Body contains too many PR/issue references (looks like incident log)",
19
+ severity: "warning",
20
+ check: (skill: Skill): LintResult | null => {
21
+ // Count PR/issue references like #123, PR #456, issue #789
22
+ const prIssueRefs = skill.body.match(/(?:PR|issue|#)\s*\d+/gi) ?? [];
23
+ if (prIssueRefs.length > 5) {
24
+ return {
25
+ rule: "incident-log-shape",
26
+ severity: "warning",
27
+ message: `Found ${prIssueRefs.length} PR/issue references. Skills should capture rules, not incident logs.`,
28
+ };
29
+ }
30
+ return null;
31
+ },
32
+ },
33
+
34
+ {
35
+ name: "references-sprawl",
36
+ description: "Too many reference files (>60)",
37
+ severity: "error",
38
+ check: (skill: Skill): LintResult | null => {
39
+ if (skill.references.length > 60) {
40
+ return {
41
+ rule: "references-sprawl",
42
+ severity: "error",
43
+ message: `Found ${skill.references.length} reference files. Skills should consolidate references.`,
44
+ };
45
+ }
46
+ return null;
47
+ },
48
+ },
49
+
50
+ {
51
+ name: "oversized-body",
52
+ description: "SKILL.md exceeds 24k characters",
53
+ severity: "warning",
54
+ check: (skill: Skill): LintResult | null => {
55
+ if (skill.body.length > 24000) {
56
+ return {
57
+ rule: "oversized-body",
58
+ severity: "warning",
59
+ message: `SKILL.md body is ${skill.body.length} chars (exceeds 24k). Consider splitting into references.`,
60
+ };
61
+ }
62
+ return null;
63
+ },
64
+ },
65
+
66
+ {
67
+ name: "missing-pitfalls",
68
+ description: "No pitfalls with 'why' explanations",
69
+ severity: "info",
70
+ check: (skill: Skill): LintResult | null => {
71
+ const hasPitfalls = skill.hermes?.pitfalls && skill.hermes.pitfalls.length > 0;
72
+ const hasWhy = skill.body.toLowerCase().includes("why");
73
+
74
+ if (!hasPitfalls && !hasWhy) {
75
+ return {
76
+ rule: "missing-pitfalls",
77
+ severity: "info",
78
+ message: "No pitfalls found. Consider documenting common mistakes and why they happen.",
79
+ };
80
+ }
81
+ return null;
82
+ },
83
+ },
84
+
85
+ {
86
+ name: "missing-examples",
87
+ description: "No examples provided",
88
+ severity: "info",
89
+ check: (skill: Skill): LintResult | null => {
90
+ if (skill.examples.length === 0) {
91
+ return {
92
+ rule: "missing-examples",
93
+ severity: "info",
94
+ message: "No examples found. Consider adding example files to demonstrate the skill.",
95
+ };
96
+ }
97
+ return null;
98
+ },
99
+ },
100
+
101
+ // === pi.dev Compatibility Rules ===
102
+
103
+ {
104
+ name: "missing-description",
105
+ description: "No description in frontmatter",
106
+ severity: "error",
107
+ check: (skill: Skill): LintResult | null => {
108
+ if (!skill.frontmatter.description || skill.frontmatter.description.length < 10) {
109
+ return {
110
+ rule: "missing-description",
111
+ severity: "error",
112
+ message: "Missing or too short description. The description determines when the model considers loading the skill.",
113
+ };
114
+ }
115
+ return null;
116
+ },
117
+ },
118
+
119
+ {
120
+ name: "invalid-name",
121
+ description: "Name contains invalid characters",
122
+ severity: "error",
123
+ check: (skill: Skill): LintResult | null => {
124
+ const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
125
+ if (!nameRegex.test(skill.frontmatter.name)) {
126
+ return {
127
+ rule: "invalid-name",
128
+ severity: "error",
129
+ message: `Invalid skill name '${skill.frontmatter.name}'. Must be kebab-case (e.g., 'code-review').`,
130
+ };
131
+ }
132
+ return null;
133
+ },
134
+ },
135
+
136
+ {
137
+ name: "name-mismatch",
138
+ description: "Directory name doesn't match skill name",
139
+ severity: "warning",
140
+ check: (skill: Skill): LintResult | null => {
141
+ const dirName = skill.path.split("/").pop() ?? "";
142
+ if (dirName !== skill.frontmatter.name && dirName !== "") {
143
+ return {
144
+ rule: "name-mismatch",
145
+ severity: "warning",
146
+ message: `Directory name '${dirName}' differs from skill name '${skill.frontmatter.name}'. Consider matching them for portability.`,
147
+ };
148
+ }
149
+ return null;
150
+ },
151
+ },
152
+
153
+ // === Hermes-Specific Rules ===
154
+
155
+ {
156
+ name: "missing-triggers",
157
+ description: "No triggers defined for auto-matching",
158
+ severity: "info",
159
+ check: (skill: Skill): LintResult | null => {
160
+ const hasTriggers = skill.hermes?.triggers && skill.hermes.triggers.length > 0;
161
+ if (!hasTriggers) {
162
+ return {
163
+ rule: "missing-triggers",
164
+ severity: "info",
165
+ message: "No triggers defined. The skill relies only on description matching.",
166
+ };
167
+ }
168
+ return null;
169
+ },
170
+ },
171
+
172
+ {
173
+ name: "low-confidence",
174
+ description: "Skill has low confidence score",
175
+ severity: "info",
176
+ check: (skill: Skill): LintResult | null => {
177
+ const confidence = skill.hermes?.confidence;
178
+ if (confidence !== undefined && confidence < 0.5) {
179
+ return {
180
+ rule: "low-confidence",
181
+ severity: "info",
182
+ message: `Low confidence score (${confidence}). Consider validating this skill pattern.`,
183
+ };
184
+ }
185
+ return null;
186
+ },
187
+ },
188
+
189
+ {
190
+ name: "unused-skill",
191
+ description: "Skill has never been used",
192
+ severity: "info",
193
+ check: (skill: Skill): LintResult | null => {
194
+ const usageCount = skill.hermes?.usage_count ?? 0;
195
+ if (usageCount === 0 && skill.hermes?.created_at) {
196
+ return {
197
+ rule: "unused-skill",
198
+ severity: "info",
199
+ message: "This skill has never been used. Consider testing or archiving it.",
200
+ };
201
+ }
202
+ return null;
203
+ },
204
+ },
205
+
206
+ {
207
+ name: "generic-description",
208
+ description: "Description is too generic",
209
+ severity: "warning",
210
+ check: (skill: Skill): LintResult | null => {
211
+ const genericPhrases = [
212
+ "helps with",
213
+ "useful for",
214
+ "does things",
215
+ "stuff",
216
+ "things",
217
+ ];
218
+ const descLower = skill.frontmatter.description.toLowerCase();
219
+
220
+ for (const phrase of genericPhrases) {
221
+ if (descLower.includes(phrase)) {
222
+ return {
223
+ rule: "generic-description",
224
+ severity: "warning",
225
+ message: `Description contains generic phrase '${phrase}'. Be specific about what the skill does and when to use it.`,
226
+ };
227
+ }
228
+ }
229
+ return null;
230
+ },
231
+ },
232
+
233
+ {
234
+ name: "log-like-content",
235
+ description: "Body contains log-like content (timestamps, repeated patterns)",
236
+ severity: "warning",
237
+ check: (skill: Skill): LintResult | null => {
238
+ // Check for log patterns
239
+ const logPatterns = [
240
+ /\d{4}-\d{2}-\d{2}/, // dates
241
+ /\d{2}:\d{2}:\d{2}/, // timestamps
242
+ /^(?:INFO|ERROR|WARN|DEBUG)/m, // log levels
243
+ /^\d+\.\s/m, // numbered lists (too detailed)
244
+ ];
245
+
246
+ let matchCount = 0;
247
+ for (const pattern of logPatterns) {
248
+ if (pattern.test(skill.body)) {
249
+ matchCount++;
250
+ }
251
+ }
252
+
253
+ if (matchCount >= 2) {
254
+ return {
255
+ rule: "log-like-content",
256
+ severity: "warning",
257
+ message: "Body contains log-like patterns. Skills should capture procedures and rules, not logs.",
258
+ };
259
+ }
260
+ return null;
261
+ },
262
+ },
263
+ ];
264
+
265
+ /**
266
+ * Lint a skill
267
+ */
268
+ export function lintSkill(skill: Skill, rules?: LintRule[]): LintResult[] {
269
+ const rulesToUse = rules ?? LINTER_RULES;
270
+ const results: LintResult[] = [];
271
+
272
+ for (const rule of rulesToUse) {
273
+ try {
274
+ const result = rule.check(skill);
275
+ if (result) {
276
+ results.push(result);
277
+ }
278
+ } catch (err) {
279
+ results.push({
280
+ rule: rule.name,
281
+ severity: "error",
282
+ message: `Linter error: ${err instanceof Error ? err.message : String(err)}`,
283
+ });
284
+ }
285
+ }
286
+
287
+ return results;
288
+ }
289
+
290
+ /**
291
+ * Lint a skill and return formatted output
292
+ */
293
+ export function lintSkillFormatted(skill: Skill, rules?: LintRule[]): string {
294
+ const results = lintSkill(skill, rules);
295
+
296
+ if (results.length === 0) {
297
+ return `✓ ${skill.id}: No issues found`;
298
+ }
299
+
300
+ const lines: string[] = [`✗ ${skill.id}:`];
301
+
302
+ for (const result of results) {
303
+ const icon = result.severity === "error" ? "✗" : result.severity === "warning" ? "⚠" : "ℹ";
304
+ lines.push(` ${icon} [${result.severity}] ${result.rule}: ${result.message}`);
305
+ }
306
+
307
+ return lines.join("\n");
308
+ }
309
+
310
+ /**
311
+ * Get lint summary for multiple skills
312
+ */
313
+ export function lintSkillsSummary(skills: Skill[]): {
314
+ total: number;
315
+ passed: number;
316
+ failed: number;
317
+ bySeverity: Record<LintSeverity, number>;
318
+ byRule: Record<string, number>;
319
+ } {
320
+ const summary = {
321
+ total: skills.length,
322
+ passed: 0,
323
+ failed: 0,
324
+ bySeverity: { error: 0, warning: 0, info: 0 } as Record<LintSeverity, number>,
325
+ byRule: {} as Record<string, number>,
326
+ };
327
+
328
+ for (const skill of skills) {
329
+ const results = lintSkill(skill);
330
+
331
+ if (results.length === 0) {
332
+ summary.passed++;
333
+ } else {
334
+ summary.failed++;
335
+ for (const result of results) {
336
+ summary.bySeverity[result.severity]++;
337
+ summary.byRule[result.rule] = (summary.byRule[result.rule] ?? 0) + 1;
338
+ }
339
+ }
340
+ }
341
+
342
+ return summary;
343
+ }
344
+
345
+ /**
346
+ * Create custom lint rule
347
+ */
348
+ export function createLintRule(
349
+ name: string,
350
+ description: string,
351
+ severity: LintSeverity,
352
+ check: (skill: Skill) => LintResult | null
353
+ ): LintRule {
354
+ return { name, description, severity, check };
355
+ }
356
+
357
+ /**
358
+ * Filter lint results by severity
359
+ */
360
+ export function filterLintResults(
361
+ results: LintResult[],
362
+ minSeverity: LintSeverity
363
+ ): LintResult[] {
364
+ const severityOrder: LintSeverity[] = ["info", "warning", "error"];
365
+ const minIndex = severityOrder.indexOf(minSeverity);
366
+
367
+ return results.filter((result) => {
368
+ const resultIndex = severityOrder.indexOf(result.severity);
369
+ return resultIndex >= minIndex;
370
+ });
371
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Skill Registry
3
+ *
4
+ * Stores and manages skills with progressive disclosure.
5
+ * Based on pi.dev Agent Skills specification.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import type {
11
+ Skill,
12
+ SkillIndex,
13
+ SkillIndexEntry,
14
+ MatchOptions,
15
+ MatchResult,
16
+ LoadOptions,
17
+ LoadedSkill,
18
+ DEFAULT_SKILLS_CONFIG,
19
+ } from "./types.js";
20
+ import { parseSkillFile, scanSkillDirectory } from "./parser.js";
21
+
22
+ /**
23
+ * Skill Registry - manages loaded and indexed skills
24
+ */
25
+ export class SkillRegistry {
26
+ private skills: Map<string, Skill> = new Map();
27
+ private index: SkillIndex = { entries: [], loadedAt: 0 };
28
+ private loading: Map<string, Promise<Skill>> = new Map();
29
+
30
+ /**
31
+ * Register a skill
32
+ */
33
+ register(skill: Skill): void {
34
+ this.skills.set(skill.id, skill);
35
+ this.updateIndex(skill);
36
+ }
37
+
38
+ /**
39
+ * Unregister a skill by ID
40
+ */
41
+ unregister(skillId: string): boolean {
42
+ const skill = this.skills.get(skillId);
43
+ if (skill) {
44
+ this.skills.delete(skillId);
45
+ this.index.entries = this.index.entries.filter(
46
+ (e) => e.id !== skillId
47
+ );
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Get a skill by ID
55
+ */
56
+ get(skillId: string): Skill | undefined {
57
+ return this.skills.get(skillId);
58
+ }
59
+
60
+ /**
61
+ * List all registered skills
62
+ */
63
+ list(): Skill[] {
64
+ return Array.from(this.skills.values());
65
+ }
66
+
67
+ /**
68
+ * Get skill index (lightweight)
69
+ */
70
+ getIndex(): SkillIndex {
71
+ return this.index;
72
+ }
73
+
74
+ /**
75
+ * Find skills matching a trigger/query
76
+ */
77
+ find(query: string, options?: MatchOptions): MatchResult[] {
78
+ const results: MatchResult[] = [];
79
+ const threshold = options?.threshold ?? 0.3;
80
+ const queryLower = query.toLowerCase();
81
+
82
+ for (const skill of this.skills.values()) {
83
+ // Skip disabled skills if requested
84
+ if (
85
+ !options?.includeDisabled &&
86
+ skill.frontmatter.disable_model_invocation
87
+ ) {
88
+ continue;
89
+ }
90
+
91
+ let score = 0;
92
+ const matchedTriggers: string[] = [];
93
+
94
+ // Check name match
95
+ if (skill.id.toLowerCase().includes(queryLower)) {
96
+ score += 0.8;
97
+ matchedTriggers.push(`name:${skill.id}`);
98
+ }
99
+
100
+ // Check description match
101
+ if (skill.frontmatter.description.toLowerCase().includes(queryLower)) {
102
+ score += 0.5;
103
+ matchedTriggers.push("description");
104
+ }
105
+
106
+ // Check triggers
107
+ if (skill.hermes?.triggers) {
108
+ for (const trigger of skill.hermes.triggers) {
109
+ if (trigger.toLowerCase().includes(queryLower)) {
110
+ score += 0.9;
111
+ matchedTriggers.push(`trigger:${trigger}`);
112
+ }
113
+ // Also check regex-style triggers
114
+ try {
115
+ const regex = new RegExp(trigger, "i");
116
+ if (regex.test(query)) {
117
+ score += 0.9;
118
+ matchedTriggers.push(`regex:${trigger}`);
119
+ }
120
+ } catch {
121
+ // Invalid regex, skip
122
+ }
123
+ }
124
+ }
125
+
126
+ // Check tags
127
+ if (skill.hermes?.anti_patterns) {
128
+ for (const anti of skill.hermes.anti_patterns) {
129
+ if (anti.toLowerCase().includes(queryLower)) {
130
+ score += 0.3;
131
+ matchedTriggers.push("anti-pattern");
132
+ }
133
+ }
134
+ }
135
+
136
+ // Usage bonus
137
+ if (skill.hermes?.usage_count && skill.hermes.usage_count > 0) {
138
+ score += Math.min(0.2, skill.hermes.usage_count * 0.01);
139
+ }
140
+
141
+ if (score >= threshold) {
142
+ results.push({ skill, score, matchedTriggers });
143
+ }
144
+ }
145
+
146
+ // Sort results
147
+ const sortBy = options?.sortBy ?? "score";
148
+ results.sort((a, b) => {
149
+ if (sortBy === "score") return b.score - a.score;
150
+ if (sortBy === "usage") {
151
+ const aUsage = a.skill.hermes?.usage_count ?? 0;
152
+ const bUsage = b.skill.hermes?.usage_count ?? 0;
153
+ return bUsage - aUsage;
154
+ }
155
+ return a.skill.id.localeCompare(b.skill.id);
156
+ });
157
+
158
+ return results;
159
+ }
160
+
161
+ /**
162
+ * Find best matching skill for a query
163
+ */
164
+ findBestMatch(query: string, options?: MatchOptions): MatchResult | null {
165
+ const results = this.find(query, { ...options, threshold: 0 });
166
+ return results[0] ?? null;
167
+ }
168
+
169
+ /**
170
+ * Load a skill's full content (progressive disclosure)
171
+ */
172
+ async load(skillId: string, options?: LoadOptions): Promise<LoadedSkill | null> {
173
+ const skill = this.skills.get(skillId);
174
+ if (!skill) {
175
+ return null;
176
+ }
177
+
178
+ // If already loaded, return cached
179
+ if (skill.loaded && !options?.loadReferences && !options?.loadExamples) {
180
+ return skill as LoadedSkill;
181
+ }
182
+
183
+ // Check if already loading
184
+ if (this.loading.has(skillId)) {
185
+ return this.loading.get(skillId) as Promise<Skill>;
186
+ }
187
+
188
+ // Load the skill
189
+ const loadPromise = this.loadSkill(skill, options);
190
+ this.loading.set(skillId, loadPromise);
191
+
192
+ try {
193
+ const loadedSkill = await loadPromise;
194
+ this.loading.delete(skillId);
195
+ return loadedSkill;
196
+ } catch (err) {
197
+ this.loading.delete(skillId);
198
+ throw err;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Load skill content from disk
204
+ */
205
+ private async loadSkill(
206
+ skill: Skill,
207
+ options?: LoadOptions
208
+ ): Promise<LoadedSkill> {
209
+ const loadedSkill: LoadedSkill = { ...skill };
210
+ const skillMdPath = join(skill.path, "SKILL.md");
211
+
212
+ // Re-parse to get fresh content
213
+ if (existsSync(skillMdPath)) {
214
+ const parsed = parseSkillFile(skillMdPath);
215
+ loadedSkill.body = parsed.body;
216
+ loadedSkill.frontmatter = parsed.frontmatter;
217
+ loadedSkill.hermes = parsed.hermes;
218
+ }
219
+
220
+ // Load references if requested
221
+ if (options?.loadReferences) {
222
+ loadedSkill.referencesContent = new Map();
223
+ for (const ref of skill.references) {
224
+ if (existsSync(ref.path)) {
225
+ try {
226
+ const content = readFileSync(ref.path, "utf-8");
227
+ loadedSkill.referencesContent.set(ref.name, content);
228
+ } catch {
229
+ // Skip unreadable files
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ // Load examples if requested
236
+ if (options?.loadExamples) {
237
+ loadedSkill.examplesContent = new Map();
238
+ for (const example of skill.examples) {
239
+ if (existsSync(example.path)) {
240
+ try {
241
+ const content = readFileSync(example.path, "utf-8");
242
+ loadedSkill.examplesContent.set(example.name, content);
243
+ } catch {
244
+ // Skip unreadable files
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ // Mark as loaded
251
+ loadedSkill.loaded = true;
252
+ loadedSkill.lastUsed = Date.now();
253
+
254
+ // Update usage count
255
+ if (loadedSkill.hermes) {
256
+ loadedSkill.hermes.usage_count =
257
+ (loadedSkill.hermes.usage_count ?? 0) + 1;
258
+ }
259
+
260
+ // Update in registry
261
+ this.skills.set(skill.id, loadedSkill);
262
+
263
+ return loadedSkill;
264
+ }
265
+
266
+ /**
267
+ * Update skill index entry
268
+ */
269
+ private updateIndex(skill: Skill): void {
270
+ // Remove existing entry if present
271
+ this.index.entries = this.index.entries.filter(
272
+ (e) => e.id !== skill.id
273
+ );
274
+
275
+ // Add new entry
276
+ this.index.entries.push({
277
+ id: skill.id,
278
+ name: skill.frontmatter.name,
279
+ description: skill.frontmatter.description,
280
+ path: skill.path,
281
+ version: skill.frontmatter.version,
282
+ triggers: skill.hermes?.triggers,
283
+ loaded: skill.loaded,
284
+ });
285
+
286
+ // Update load time
287
+ this.index.loadedAt = Date.now();
288
+ }
289
+
290
+ /**
291
+ * Clear all skills
292
+ */
293
+ clear(): void {
294
+ this.skills.clear();
295
+ this.index = { entries: [], loadedAt: 0 };
296
+ this.loading.clear();
297
+ }
298
+
299
+ /**
300
+ * Get count of registered skills
301
+ */
302
+ get size(): number {
303
+ return this.skills.size;
304
+ }
305
+
306
+ /**
307
+ * Check if skill exists
308
+ */
309
+ has(skillId: string): boolean {
310
+ return this.skills.has(skillId);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Create a new skill registry
316
+ */
317
+ export function createSkillRegistry(): SkillRegistry {
318
+ return new SkillRegistry();
319
+ }
320
+
321
+ /**
322
+ * Global skill registry instance (singleton)
323
+ */
324
+ let globalRegistry: SkillRegistry | null = null;
325
+
326
+ export function getGlobalSkillRegistry(): SkillRegistry {
327
+ if (!globalRegistry) {
328
+ globalRegistry = createSkillRegistry();
329
+ }
330
+ return globalRegistry;
331
+ }
332
+
333
+ export function setGlobalSkillRegistry(registry: SkillRegistry): void {
334
+ globalRegistry = registry;
335
+ }