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.
- package/CHANGELOG.md +33 -0
- package/harness/loop-runtime.ts +3 -0
- package/index.ts +130 -29
- package/package.json +2 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/dist/client.d.ts +51 -0
- package/packages/qdrant-skills/dist/client.d.ts.map +1 -0
- package/packages/qdrant-skills/dist/client.js +105 -0
- package/packages/qdrant-skills/dist/client.js.map +1 -0
- package/packages/qdrant-skills/dist/embedder.d.ts +39 -0
- package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -0
- package/packages/qdrant-skills/dist/embedder.js +101 -0
- package/packages/qdrant-skills/dist/embedder.js.map +1 -0
- package/packages/qdrant-skills/dist/index.d.ts +25 -0
- package/packages/qdrant-skills/dist/index.d.ts.map +1 -0
- package/packages/qdrant-skills/dist/index.js +25 -0
- package/packages/qdrant-skills/dist/index.js.map +1 -0
- package/packages/qdrant-skills/package.json +27 -0
- package/packages/qdrant-skills/src/client.ts +178 -0
- package/packages/qdrant-skills/src/embedder.ts +137 -0
- package/packages/qdrant-skills/src/index.ts +42 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +40 -0
- package/packages/skills/dist/index.d.ts.map +1 -0
- package/packages/skills/dist/index.js +84 -0
- package/packages/skills/dist/index.js.map +1 -0
- package/packages/skills/dist/linter.d.ts +38 -0
- package/packages/skills/dist/linter.d.ts.map +1 -0
- package/packages/skills/dist/linter.js +323 -0
- package/packages/skills/dist/linter.js.map +1 -0
- package/packages/skills/dist/loader.d.ts +74 -0
- package/packages/skills/dist/loader.d.ts.map +1 -0
- package/packages/skills/dist/loader.js +280 -0
- package/packages/skills/dist/loader.js.map +1 -0
- package/packages/skills/dist/parser.d.ts +32 -0
- package/packages/skills/dist/parser.d.ts.map +1 -0
- package/packages/skills/dist/parser.js +459 -0
- package/packages/skills/dist/parser.js.map +1 -0
- package/packages/skills/dist/scanner.d.ts +33 -0
- package/packages/skills/dist/scanner.d.ts.map +1 -0
- package/packages/skills/dist/scanner.js +257 -0
- package/packages/skills/dist/scanner.js.map +1 -0
- package/packages/skills/dist/skill-commands.d.ts +33 -0
- package/packages/skills/dist/skill-commands.d.ts.map +1 -0
- package/packages/skills/dist/skill-commands.js +239 -0
- package/packages/skills/dist/skill-commands.js.map +1 -0
- package/packages/skills/dist/skill-tool.d.ts +120 -0
- package/packages/skills/dist/skill-tool.d.ts.map +1 -0
- package/packages/skills/dist/skill-tool.js +239 -0
- package/packages/skills/dist/skill-tool.js.map +1 -0
- package/packages/skills/dist/skill-versioning.d.ts +123 -0
- package/packages/skills/dist/skill-versioning.d.ts.map +1 -0
- package/packages/skills/dist/skill-versioning.js +240 -0
- package/packages/skills/dist/skill-versioning.js.map +1 -0
- package/packages/skills/dist/types.d.ts +226 -0
- package/packages/skills/dist/types.d.ts.map +1 -0
- package/packages/skills/dist/types.js +21 -0
- package/packages/skills/dist/types.js.map +1 -0
- package/packages/skills/package.json +46 -0
- package/packages/skills/src/index.ts +95 -0
- package/packages/skills/src/linter.ts +371 -0
- package/packages/skills/src/loader.ts +335 -0
- package/packages/skills/src/parser.ts +548 -0
- package/packages/skills/src/scanner.ts +322 -0
- package/packages/skills/src/skill-commands.ts +318 -0
- package/packages/skills/src/skill-tool.ts +324 -0
- package/packages/skills/src/skill-versioning.ts +345 -0
- package/packages/skills/src/types.ts +259 -0
- package/packages/skills/test/init-skills.test.ts +118 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/curator.d.ts +189 -0
- package/packages/trajectory/dist/src/curator.d.ts.map +1 -0
- package/packages/trajectory/dist/src/curator.js +340 -0
- package/packages/trajectory/dist/src/curator.js.map +1 -0
- package/packages/trajectory/dist/src/journey-renderer.d.ts +53 -0
- package/packages/trajectory/dist/src/journey-renderer.d.ts.map +1 -0
- package/packages/trajectory/dist/src/journey-renderer.js +324 -0
- package/packages/trajectory/dist/src/journey-renderer.js.map +1 -0
- package/packages/trajectory/dist/src/learning-graph.d.ts +159 -0
- package/packages/trajectory/dist/src/learning-graph.d.ts.map +1 -0
- package/packages/trajectory/dist/src/learning-graph.js +258 -0
- package/packages/trajectory/dist/src/learning-graph.js.map +1 -0
- package/packages/trajectory/dist/src/memory-store.d.ts +146 -0
- package/packages/trajectory/dist/src/memory-store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/memory-store.js +472 -0
- package/packages/trajectory/dist/src/memory-store.js.map +1 -0
- package/packages/trajectory/dist/src/review-fork.d.ts +164 -0
- package/packages/trajectory/dist/src/review-fork.d.ts.map +1 -0
- package/packages/trajectory/dist/src/review-fork.js +332 -0
- package/packages/trajectory/dist/src/review-fork.js.map +1 -0
- package/packages/trajectory/dist/src/write-approval.d.ts +136 -0
- package/packages/trajectory/dist/src/write-approval.d.ts.map +1 -0
- package/packages/trajectory/dist/src/write-approval.js +281 -0
- package/packages/trajectory/dist/src/write-approval.js.map +1 -0
- package/packages/trajectory/package.json +1 -1
- package/packages/trajectory/src/curator.ts +485 -0
- package/packages/trajectory/src/journey-renderer.ts +399 -0
- package/packages/trajectory/src/learning-graph.ts +363 -0
- package/packages/trajectory/src/memory-store.ts +578 -0
- package/packages/trajectory/src/review-fork.ts +450 -0
- package/packages/trajectory/src/write-approval.ts +372 -0
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/dist/blackboard.d.ts +98 -0
- package/packages/write-review/dist/blackboard.d.ts.map +1 -0
- package/packages/write-review/dist/blackboard.js +289 -0
- package/packages/write-review/dist/blackboard.js.map +1 -0
- package/packages/write-review/dist/event-bus.d.ts +106 -0
- package/packages/write-review/dist/event-bus.d.ts.map +1 -0
- package/packages/write-review/dist/event-bus.js +294 -0
- package/packages/write-review/dist/event-bus.js.map +1 -0
- package/packages/write-review/dist/gate.d.ts +37 -0
- package/packages/write-review/dist/gate.d.ts.map +1 -0
- package/packages/write-review/dist/gate.js +138 -0
- package/packages/write-review/dist/gate.js.map +1 -0
- package/packages/write-review/dist/index.d.ts +22 -0
- package/packages/write-review/dist/index.d.ts.map +1 -0
- package/packages/write-review/dist/index.js +60 -0
- package/packages/write-review/dist/index.js.map +1 -0
- package/packages/write-review/dist/injection.d.ts +40 -0
- package/packages/write-review/dist/injection.d.ts.map +1 -0
- package/packages/write-review/dist/injection.js +179 -0
- package/packages/write-review/dist/injection.js.map +1 -0
- package/packages/write-review/dist/review.d.ts +54 -0
- package/packages/write-review/dist/review.d.ts.map +1 -0
- package/packages/write-review/dist/review.js +154 -0
- package/packages/write-review/dist/review.js.map +1 -0
- package/packages/write-review/dist/trigger.d.ts +39 -0
- package/packages/write-review/dist/trigger.d.ts.map +1 -0
- package/packages/write-review/dist/trigger.js +107 -0
- package/packages/write-review/dist/trigger.js.map +1 -0
- package/packages/write-review/dist/types.d.ts +78 -0
- package/packages/write-review/dist/types.d.ts.map +1 -0
- package/packages/write-review/dist/types.js +26 -0
- package/packages/write-review/dist/types.js.map +1 -0
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/event-bus.ts +351 -0
- package/packages/write-review/src/index.ts +1 -0
- package/packages/write-review/src/injection.ts +25 -3
- package/proactive-compact.ts +51 -7
- package/scripts/sync-skills.ts +275 -0
- 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
|
+
}
|