cyber-skills 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cyber-skills.mts +102 -0
- package/hooks/definitions/commit-discipline.mts +15 -0
- package/hooks/definitions/init.mts +22 -0
- package/hooks/inject-commit-discipline.mts +66 -0
- package/hooks/lib/commit-discipline-content.mts +53 -0
- package/hooks/lib/hook-command.mts +31 -0
- package/hooks/lib/package-root.mts +7 -0
- package/hooks/register-agent-hooks.mts +290 -0
- package/hooks/runtime/commit-discipline.mts +39 -0
- package/package.json +57 -0
- package/readme.md +76 -0
- package/skills/audit-skill/SKILL.md +271 -0
- package/skills/audit-skill/scripts/validate-skills.mts +495 -0
- package/skills/commit/SKILL.md +34 -0
- package/skills/configure-awesome-sources/SKILL.md +73 -0
- package/skills/configure-awesome-sources/scripts/configure-awesome-sources.mts +252 -0
- package/skills/create-skill/SKILL.md +126 -0
- package/skills/find-awesome-skill/SKILL.md +55 -0
- package/skills/find-awesome-skill/scripts/awesome-lib.mts +476 -0
- package/skills/find-awesome-skill/scripts/find-awesome-skill.mts +39 -0
- package/skills/init/SKILL.md +83 -0
- package/skills/init-commit-discipline/SKILL.md +75 -0
- package/skills/init-commit-discipline/scripts/resolve-commit-skill.mts +76 -0
- package/skills/patch-skill/SKILL.md +229 -0
- package/skills/skillify/SKILL.md +110 -0
- package/skills/update-awesome-list/SKILL.md +65 -0
- package/skills/update-awesome-list/scripts/inspect-skills-repo.mts +112 -0
- package/skills/update-awesome-list/scripts/render-awesome-list.mts +91 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
|
|
7
|
+
|
|
8
|
+
interface Finding {
|
|
9
|
+
severity: Severity
|
|
10
|
+
checkId: string
|
|
11
|
+
name: string
|
|
12
|
+
evidence: string
|
|
13
|
+
fix: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CheckResult {
|
|
17
|
+
criticals: Finding[]
|
|
18
|
+
warnings: Finding[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SKILL_DIRS = ['skills', '.agents/skills']
|
|
22
|
+
|
|
23
|
+
const GENERIC_PHRASES = [
|
|
24
|
+
'helps with',
|
|
25
|
+
'general purpose',
|
|
26
|
+
'handles tasks',
|
|
27
|
+
'use this skill when the user asks anything',
|
|
28
|
+
'does things',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const E1_PATTERNS: RegExp[] = [
|
|
32
|
+
/rm\s+-[rRf]*f[rRf]*\s+/,
|
|
33
|
+
/sudo\s+rm/,
|
|
34
|
+
/curl[^|\n]*\|\s*(ba)?sh/,
|
|
35
|
+
/wget[^|\n]*\|\s*(ba)?sh/,
|
|
36
|
+
/\bdd\s+if=/,
|
|
37
|
+
/\b(mkfs|fdisk|parted)\b/,
|
|
38
|
+
/kill\s+-9\s+1\b/,
|
|
39
|
+
/:\(\)\{\s*:\|:&\s*\}/,
|
|
40
|
+
/chmod\s+-R\s+777\s+/,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const E2_PATTERNS: RegExp[] = [
|
|
44
|
+
/[Ii]gnore (previous|all|prior) instructions/,
|
|
45
|
+
/[Yy]ou are now [A-Z][a-zA-Z]/,
|
|
46
|
+
/[Ff]rom now on you are /,
|
|
47
|
+
/[Dd]isregard your (guidelines|rules)/,
|
|
48
|
+
/[Ff]orget your (guidelines|training)/,
|
|
49
|
+
/[Yy]our new instructions are/,
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function findSkillFiles(dirs: string[], cwd: string): string[] {
|
|
55
|
+
const seen = new Set<string>()
|
|
56
|
+
const results: string[] = []
|
|
57
|
+
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
const base = path.join(cwd, dir)
|
|
60
|
+
if (!fs.existsSync(base)) continue
|
|
61
|
+
|
|
62
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
63
|
+
const skillFile = path.join(base, entry.name, 'SKILL.md')
|
|
64
|
+
if (!fs.existsSync(skillFile)) continue
|
|
65
|
+
try {
|
|
66
|
+
const real = fs.realpathSync(skillFile)
|
|
67
|
+
if (!seen.has(real)) {
|
|
68
|
+
seen.add(real)
|
|
69
|
+
results.push(real)
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// skip unresolvable symlinks
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return results.sort()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseFrontmatter(content: string): { name: string; description: string } {
|
|
81
|
+
const lines = content.split('\n')
|
|
82
|
+
let fmCount = 0
|
|
83
|
+
let name = ''
|
|
84
|
+
let description = ''
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.trim() === '---') {
|
|
88
|
+
fmCount++
|
|
89
|
+
if (fmCount === 2) break
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (fmCount !== 1) continue
|
|
93
|
+
|
|
94
|
+
const nameMatch = line.match(/^name:\s*(.+)/)
|
|
95
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '')
|
|
96
|
+
|
|
97
|
+
const descMatch = line.match(/^description:\s*(.+)/)
|
|
98
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, '')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { name, description }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractBody(content: string): string {
|
|
105
|
+
const lines = content.split('\n')
|
|
106
|
+
let fmCount = 0
|
|
107
|
+
const bodyLines: string[] = []
|
|
108
|
+
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (line.trim() === '---') {
|
|
111
|
+
fmCount++
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if (fmCount >= 2) bodyLines.push(line)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return bodyLines.join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractCodeBlocks(content: string): string {
|
|
121
|
+
const lines = content.split('\n')
|
|
122
|
+
let inBlock = false
|
|
123
|
+
const out: string[] = []
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
if (lines[i].startsWith('```')) {
|
|
127
|
+
inBlock = !inBlock
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (inBlock) out.push(`${i + 1}: ${lines[i]}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return out.join('\n')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Removes inline `backtick` spans and "double-quoted" strings so that
|
|
137
|
+
// documentation examples don't trigger security/injection checks.
|
|
138
|
+
function stripExamples(content: string): string {
|
|
139
|
+
return content.replace(/`[^`]*`/g, '').replace(/"[^"]*"/g, '')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isShellExpandedReference(source: string, matchIndex: number): boolean {
|
|
143
|
+
const previousChar = source[matchIndex - 1]
|
|
144
|
+
const previousTwoChars = source.slice(Math.max(0, matchIndex - 2), matchIndex)
|
|
145
|
+
return previousChar === '$' || previousTwoChars === ')/' || previousTwoChars === '}/'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Check runner ─────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export function runChecks(filePath: string): CheckResult {
|
|
151
|
+
const criticals: Finding[] = []
|
|
152
|
+
const warnings: Finding[] = []
|
|
153
|
+
|
|
154
|
+
const crit = (checkId: string, name: string, evidence: string, fix: string) =>
|
|
155
|
+
criticals.push({ severity: 'CRITICAL', checkId, name, evidence, fix })
|
|
156
|
+
|
|
157
|
+
const warn = (severity: Severity, checkId: string, name: string, evidence: string, fix: string) =>
|
|
158
|
+
warnings.push({ severity, checkId, name, evidence, fix })
|
|
159
|
+
|
|
160
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
161
|
+
const skillDir = path.dirname(filePath)
|
|
162
|
+
const dirName = path.basename(skillDir)
|
|
163
|
+
const parent = path.basename(path.dirname(skillDir))
|
|
164
|
+
|
|
165
|
+
const { name: fmName, description: fmDesc } = parseFrontmatter(content)
|
|
166
|
+
const body = extractBody(content)
|
|
167
|
+
const codeBlocks = extractCodeBlocks(content)
|
|
168
|
+
const stripped = stripExamples(content)
|
|
169
|
+
|
|
170
|
+
// ── Structure ───────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
// S1: file must sit at <something>/skills/<name>/SKILL.md
|
|
173
|
+
if (parent !== 'skills') {
|
|
174
|
+
crit(
|
|
175
|
+
'S1',
|
|
176
|
+
'SKILL.md in own directory',
|
|
177
|
+
`path: ${filePath}`,
|
|
178
|
+
'Move SKILL.md into its own named subdirectory under a skills/ directory',
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// S2: name and description frontmatter required
|
|
183
|
+
if (!fmName) {
|
|
184
|
+
crit(
|
|
185
|
+
'S2',
|
|
186
|
+
'Required frontmatter: name',
|
|
187
|
+
'name: field missing or empty',
|
|
188
|
+
`Add 'name: ${dirName}' to the YAML frontmatter block`,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
if (!fmDesc) {
|
|
192
|
+
crit(
|
|
193
|
+
'S2',
|
|
194
|
+
'Required frontmatter: description',
|
|
195
|
+
'description: field missing or empty',
|
|
196
|
+
'Add a description: field to the YAML frontmatter block',
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// S3: name must match directory name
|
|
201
|
+
if (fmName && fmName !== dirName) {
|
|
202
|
+
warn(
|
|
203
|
+
'HIGH',
|
|
204
|
+
'S3',
|
|
205
|
+
'name matches directory',
|
|
206
|
+
`name: '${fmName}' but directory is '${dirName}'`,
|
|
207
|
+
`Set name: to '${dirName}'`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// S4: relative file paths referenced in code blocks must exist inside the skill directory.
|
|
212
|
+
// Prose examples (e.g., scripts/setup.sh) are excluded — only fenced code blocks are checked.
|
|
213
|
+
// Skips: template placeholders (<>), globs (*), absolute/home paths.
|
|
214
|
+
const s4RefPattern = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_./-]+\.[a-zA-Z]+)/g
|
|
215
|
+
// Skip repo-root-level prefixes (these are not skill-bundle references)
|
|
216
|
+
const s4Skip = /[<>*]|^(https?:\/\/|~|\/|skills\/|\.agents\/|\.github\/|\.cursor\/|\.vscode\/)/
|
|
217
|
+
const s4Ext = /\.(md|sh|mts|mjs|js|ts|py|json|yaml|yml)$/
|
|
218
|
+
const s4Refs = new Set<string>()
|
|
219
|
+
let m: RegExpExecArray | null
|
|
220
|
+
while ((m = s4RefPattern.exec(codeBlocks)) !== null) {
|
|
221
|
+
const ref = m[1]
|
|
222
|
+
if (isShellExpandedReference(codeBlocks, m.index)) continue
|
|
223
|
+
// Skip refs that are embedded within an absolute path (preceded by '/')
|
|
224
|
+
if (m.index > 0 && codeBlocks[m.index - 1] === '/') continue
|
|
225
|
+
if (!s4Skip.test(ref) && s4Ext.test(ref)) s4Refs.add(ref)
|
|
226
|
+
}
|
|
227
|
+
for (const ref of s4Refs) {
|
|
228
|
+
if (!fs.existsSync(path.join(skillDir, ref))) {
|
|
229
|
+
warn(
|
|
230
|
+
'HIGH',
|
|
231
|
+
'S4',
|
|
232
|
+
'Referenced file does not exist in skill directory',
|
|
233
|
+
`${ref} (looked for ${path.join(skillDir, ref)})`,
|
|
234
|
+
'Create the file inside the skill directory or remove the reference',
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// S5: internal anchor links must resolve to real headings.
|
|
240
|
+
// Checked on stripped content so backtick-wrapped syntax examples don't trigger.
|
|
241
|
+
const anchorPattern = /\[([^\]]+)\]\(#([^)]+)\)/g
|
|
242
|
+
while ((m = anchorPattern.exec(stripped)) !== null) {
|
|
243
|
+
const anchor = m[2]
|
|
244
|
+
const headingPat = anchor.replace(/-/g, '[- ]')
|
|
245
|
+
if (!new RegExp(`^#{1,6}.*${headingPat}`, 'im').test(content)) {
|
|
246
|
+
warn(
|
|
247
|
+
'MEDIUM',
|
|
248
|
+
'S5',
|
|
249
|
+
'Internal anchor link does not resolve',
|
|
250
|
+
`#${anchor}`,
|
|
251
|
+
`Add a heading matching '${anchor.replace(/-/g, ' ')}' or fix the link target`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Quality ──────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
// Q1: description must include trigger language
|
|
259
|
+
if (fmDesc && !/use this skill when|when to use/i.test(fmDesc)) {
|
|
260
|
+
warn(
|
|
261
|
+
'HIGH',
|
|
262
|
+
'Q1',
|
|
263
|
+
'Trigger language in description',
|
|
264
|
+
`description: ${fmDesc}`,
|
|
265
|
+
"Add 'Use this skill when ...' to the description field",
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Q2: description must be specific (≥12 words, no generic phrases)
|
|
270
|
+
if (fmDesc) {
|
|
271
|
+
const wordCount = fmDesc.split(/\s+/).filter(Boolean).length
|
|
272
|
+
if (wordCount < 12) {
|
|
273
|
+
warn(
|
|
274
|
+
'HIGH',
|
|
275
|
+
'Q2',
|
|
276
|
+
'Description too short (specificity)',
|
|
277
|
+
`${wordCount} words: ${fmDesc}`,
|
|
278
|
+
'Expand the description to at least 12 words with specific trigger conditions',
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
const genericHit = GENERIC_PHRASES.find((p) => new RegExp(p, 'i').test(fmDesc))
|
|
282
|
+
if (genericHit) {
|
|
283
|
+
warn(
|
|
284
|
+
'HIGH',
|
|
285
|
+
'Q2',
|
|
286
|
+
'Description uses vague generic phrase',
|
|
287
|
+
`matched '${genericHit}' in: ${fmDesc}`,
|
|
288
|
+
'Replace with specific trigger conditions and outcomes',
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Q3: apparent sub-skills must carry "Internal skill:" prefix
|
|
294
|
+
if (fmDesc && /called by\b|^internal\b/i.test(fmDesc) && !/^Internal skill:/i.test(fmDesc)) {
|
|
295
|
+
warn(
|
|
296
|
+
'MEDIUM',
|
|
297
|
+
'Q3',
|
|
298
|
+
"Sub-skill missing 'Internal skill:' prefix",
|
|
299
|
+
`description: ${fmDesc}`,
|
|
300
|
+
"Prefix description with 'Internal skill: ' to prevent unintended activation",
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Q4: body must contain actionable content (numbered steps or headers)
|
|
305
|
+
if (!body.trim()) {
|
|
306
|
+
warn(
|
|
307
|
+
'MEDIUM',
|
|
308
|
+
'Q4',
|
|
309
|
+
'No actionable instruction body',
|
|
310
|
+
'body is empty after frontmatter',
|
|
311
|
+
'Add numbered steps, decision logic, or ## section headers',
|
|
312
|
+
)
|
|
313
|
+
} else if (!/^\d+\.|^#{1,4} /m.test(body)) {
|
|
314
|
+
warn(
|
|
315
|
+
'MEDIUM',
|
|
316
|
+
'Q4',
|
|
317
|
+
'No actionable instruction body',
|
|
318
|
+
'body has no numbered steps or section headers',
|
|
319
|
+
'Add numbered steps, decision logic, or ## section headers',
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Q5: description must be ≤120 characters
|
|
324
|
+
if (fmDesc && fmDesc.length > 120) {
|
|
325
|
+
warn(
|
|
326
|
+
'MEDIUM',
|
|
327
|
+
'Q5',
|
|
328
|
+
'Description exceeds 120 characters',
|
|
329
|
+
`description: ${fmDesc.slice(0, 80)}… (${fmDesc.length} chars)`,
|
|
330
|
+
'Trim to ≤120 chars; move example phrases to the skill body',
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Q10: SKILL.md must not treat stdout prose/tables as authoritative data
|
|
335
|
+
const q10Patterns: RegExp[] = [
|
|
336
|
+
/show (this |the )?(summary )?table/i,
|
|
337
|
+
/parse (the )?stdout/i,
|
|
338
|
+
/from the (script )?output/i,
|
|
339
|
+
/prints a summary table/i,
|
|
340
|
+
/print(s|ed)? a (summary )?table/i,
|
|
341
|
+
]
|
|
342
|
+
const q10Line = body.split('\n').find((line) => {
|
|
343
|
+
if (/\b(do not|don't|never)\b/i.test(line) && /parse (the )?stdout/i.test(line)) return false
|
|
344
|
+
return q10Patterns.some((pat) => pat.test(line))
|
|
345
|
+
})
|
|
346
|
+
if (q10Line) {
|
|
347
|
+
warn(
|
|
348
|
+
'HIGH',
|
|
349
|
+
'Q10',
|
|
350
|
+
'SKILL.md instructs parsing stdout prose/tables as data',
|
|
351
|
+
q10Line.trim(),
|
|
352
|
+
'Point agents at an artifact file or jq on CLI output; stdout should be JSON ack only',
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Q11: interactive scripts must document --yes for agent runs
|
|
357
|
+
const scriptsDir = path.join(skillDir, 'scripts')
|
|
358
|
+
if (fs.existsSync(scriptsDir)) {
|
|
359
|
+
const scriptFiles = fs.readdirSync(scriptsDir).filter((f) => !f.startsWith('.'))
|
|
360
|
+
const hasInteractive = scriptFiles.some((f) => {
|
|
361
|
+
const src = fs.readFileSync(path.join(scriptsDir, f), 'utf8')
|
|
362
|
+
return /readline|createInterface|\.question\(/.test(src)
|
|
363
|
+
})
|
|
364
|
+
if (hasInteractive && !/--yes|-y/.test(content)) {
|
|
365
|
+
warn(
|
|
366
|
+
'HIGH',
|
|
367
|
+
'Q11',
|
|
368
|
+
'Interactive script missing non-interactive agent path',
|
|
369
|
+
'scripts/ uses readline/prompt but SKILL.md does not document --yes or -y',
|
|
370
|
+
'Document --yes (or equivalent) in SKILL.md for autonomous agent runs',
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Security ─────────────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
// E1: dangerous shell commands — checked in fenced code blocks only.
|
|
378
|
+
// Code blocks are the only place an agent would execute commands.
|
|
379
|
+
for (const pat of E1_PATTERNS) {
|
|
380
|
+
const hit = codeBlocks.split('\n').find((l) => pat.test(l))
|
|
381
|
+
if (hit) {
|
|
382
|
+
crit(
|
|
383
|
+
'E1',
|
|
384
|
+
'Dangerous shell command (in code block)',
|
|
385
|
+
hit.trim(),
|
|
386
|
+
'Remove or rewrite; never embed destructive commands in a skill',
|
|
387
|
+
)
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// E2: prompt injection patterns — checked on prose with examples stripped.
|
|
393
|
+
// Inline backtick/quoted examples are documentation, not instructions.
|
|
394
|
+
for (const pat of E2_PATTERNS) {
|
|
395
|
+
const hit = stripped.split('\n').find((l) => pat.test(l))
|
|
396
|
+
if (hit) {
|
|
397
|
+
crit(
|
|
398
|
+
'E2',
|
|
399
|
+
'Prompt injection pattern',
|
|
400
|
+
hit.trim(),
|
|
401
|
+
'Remove prompt-injection content; treat skill body as untrusted data',
|
|
402
|
+
)
|
|
403
|
+
break
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// E6: force-push to main/master without an explicit confirmation step
|
|
408
|
+
const e6Hit = stripped.split('\n').find((l) => /git push.*(--force|-f )/.test(l) && /(main|master)/.test(l))
|
|
409
|
+
if (e6Hit) {
|
|
410
|
+
warn(
|
|
411
|
+
'HIGH',
|
|
412
|
+
'E6',
|
|
413
|
+
'Silent permission escalation: force-push to main/master',
|
|
414
|
+
e6Hit.trim(),
|
|
415
|
+
'Add an explicit user-confirmation step before the force-push',
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { criticals, warnings }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Output ───────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
function printFinding(f: Finding): void {
|
|
425
|
+
const icon = f.severity === 'CRITICAL' ? '❌' : '⚠️ '
|
|
426
|
+
console.info(` ${icon} [${f.severity}] ${f.checkId} — ${f.name}`)
|
|
427
|
+
console.info(` Evidence: ${f.evidence}`)
|
|
428
|
+
console.info(` Fix: ${f.fix}`)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
function main(): void {
|
|
434
|
+
const args = process.argv.slice(2)
|
|
435
|
+
const pathIdx = args.indexOf('--path')
|
|
436
|
+
const targetPath = pathIdx !== -1 ? args[pathIdx + 1] : undefined
|
|
437
|
+
|
|
438
|
+
const cwd = process.cwd()
|
|
439
|
+
let skillFiles: string[]
|
|
440
|
+
|
|
441
|
+
if (targetPath) {
|
|
442
|
+
const resolved = path.resolve(cwd, targetPath)
|
|
443
|
+
const skillMd = resolved.endsWith('SKILL.md') ? resolved : path.join(resolved, 'SKILL.md')
|
|
444
|
+
if (!fs.existsSync(skillMd)) {
|
|
445
|
+
console.error(`No SKILL.md found at ${skillMd}`)
|
|
446
|
+
process.exit(1)
|
|
447
|
+
}
|
|
448
|
+
skillFiles = [fs.realpathSync(skillMd)]
|
|
449
|
+
} else {
|
|
450
|
+
skillFiles = findSkillFiles(SKILL_DIRS, cwd)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (skillFiles.length === 0) {
|
|
454
|
+
console.info('No SKILL.md files found.')
|
|
455
|
+
process.exit(0)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
console.info(`Validating ${skillFiles.length} skill(s)…`)
|
|
459
|
+
|
|
460
|
+
let totalCriticals = 0
|
|
461
|
+
let totalWarnings = 0
|
|
462
|
+
|
|
463
|
+
for (const filePath of skillFiles) {
|
|
464
|
+
const dirName = path.basename(path.dirname(filePath))
|
|
465
|
+
console.info(`\n── ${dirName} ─────────────────────────`)
|
|
466
|
+
|
|
467
|
+
const { criticals, warnings } = runChecks(filePath)
|
|
468
|
+
totalCriticals += criticals.length
|
|
469
|
+
totalWarnings += warnings.length
|
|
470
|
+
|
|
471
|
+
for (const f of criticals) printFinding(f)
|
|
472
|
+
for (const f of warnings) printFinding(f)
|
|
473
|
+
|
|
474
|
+
if (criticals.length === 0) {
|
|
475
|
+
console.info(' ✅ no CRITICAL findings')
|
|
476
|
+
} else {
|
|
477
|
+
console.info(' 🚨 DO NOT commit or install until all CRITICAL findings are resolved.')
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.info('\n══════════════════════════════════════')
|
|
482
|
+
console.info(`Results: ${totalCriticals} critical failure(s), ${totalWarnings} warning(s)`)
|
|
483
|
+
|
|
484
|
+
if (totalCriticals > 0) {
|
|
485
|
+
console.info('❌ Fix all CRITICAL findings before merging.')
|
|
486
|
+
process.exit(1)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.info('✅ All checks passed (S1–S5, Q1–Q5, Q10–Q11, E1–E2, E6).')
|
|
490
|
+
console.info(' Run the audit-skill agent skill for full quality review (Q6–Q12, E3–E5, E7).')
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
494
|
+
main()
|
|
495
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
description: "Use this skill when committing and you need Conventional Commits guidance — staging, messages, one concern per commit."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Commit
|
|
7
|
+
|
|
8
|
+
Minimal commit helper for repos using commit discipline. For full staging/splitting workflows, use `commit-work` from [softaworks/agent-toolkit](https://github.com/softaworks/agent-toolkit) instead.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
|
|
12
|
+
- Commit every self-contained unit of work before moving on
|
|
13
|
+
- Use Conventional Commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`
|
|
14
|
+
- One concern per commit; never batch unrelated changes
|
|
15
|
+
- Use `git add -p` when a file has mixed changes
|
|
16
|
+
- Never commit with red tests; run the repo's validation command first
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. Inspect: `git status`, `git diff` (and `git diff --cached` after staging)
|
|
21
|
+
2. Stage only what belongs: `git add -p`
|
|
22
|
+
3. Review staged diff: `git diff --cached`
|
|
23
|
+
4. Write message: `type: short imperative summary` (optional body for why)
|
|
24
|
+
5. Commit: `git commit -m "type: summary"`
|
|
25
|
+
|
|
26
|
+
## Message format
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
type: short summary in imperative mood
|
|
30
|
+
|
|
31
|
+
Optional body explaining what changed and why.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Examples: `feat: add commit discipline hook registration`, `fix: resolve package root for run-hook`
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure-awesome-sources
|
|
3
|
+
description: Use this skill when adding, removing, or inspecting the awesome-list sources used for curated skill discovery.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Configure Awesome Sources
|
|
7
|
+
|
|
8
|
+
Manage the layered source configuration used by `find-awesome-skill`.
|
|
9
|
+
|
|
10
|
+
## Layers
|
|
11
|
+
|
|
12
|
+
Configuration files are loaded from:
|
|
13
|
+
|
|
14
|
+
1. `.agents/awesome-skill-sources.local.json`
|
|
15
|
+
2. `.agents/awesome-skill-sources.json`
|
|
16
|
+
3. `~/.agents/awesome-skill-sources.json`
|
|
17
|
+
|
|
18
|
+
The local private file should stay gitignored.
|
|
19
|
+
|
|
20
|
+
Each file uses:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"version": 1,
|
|
25
|
+
"sources": [
|
|
26
|
+
{ "repo": "owner/name", "path": "awesome-skills.json" }
|
|
27
|
+
],
|
|
28
|
+
"disabled_sources": [
|
|
29
|
+
{ "repo": "owner/name", "path": "awesome-skills.json" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `disabled_sources` to suppress inherited sources. Do not use an `enabled` field.
|
|
35
|
+
|
|
36
|
+
## Operations
|
|
37
|
+
|
|
38
|
+
List effective sources:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts list
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add a source:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts add --layer repo --repo owner/name --path awesome-skills.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Disable an inherited source:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts disable --layer repo --repo owner/name --path awesome-skills.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Enable a disabled source:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts enable --layer repo --repo owner/name --path awesome-skills.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Remove a direct source:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts remove --layer repo --repo owner/name --path awesome-skills.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Guidance
|
|
69
|
+
|
|
70
|
+
- Use `local` for repo-private personal overrides.
|
|
71
|
+
- Use `repo` for team or company shared defaults in the current repository.
|
|
72
|
+
- Use `global` for user-wide defaults.
|
|
73
|
+
- If a repo-shared config disables a source, that suppression should win over a user-global source entry.
|