@skilly-hand/skilly-hand 0.17.0 → 0.19.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/CHANGELOG.md +39 -0
- package/README.md +9 -3
- package/catalog/skills/accessibility-audit/SKILL.md +21 -0
- package/catalog/skills/agents-root-orchestrator/SKILL.md +19 -0
- package/catalog/skills/angular-guidelines/SKILL.md +21 -0
- package/catalog/skills/figma-mcp-0to1/SKILL.md +21 -0
- package/catalog/skills/frontend-design/SKILL.md +17 -0
- package/catalog/skills/output-optimizer/SKILL.md +18 -0
- package/catalog/skills/project-security/SKILL.md +19 -0
- package/catalog/skills/project-security/assets/generic-ci-security-gate.sh +1 -28
- package/catalog/skills/project-security/assets/github-actions-security-gate.yml +38 -0
- package/catalog/skills/project-security/assets/pre-commit.sample.sh +1 -1
- package/catalog/skills/project-security/assets/pre-push.sample.sh +1 -30
- package/catalog/skills/project-security/assets/run-security-check.shared.sh +33 -0
- package/catalog/skills/project-teacher/SKILL.md +17 -0
- package/catalog/skills/react-guidelines/SKILL.md +21 -0
- package/catalog/skills/review-rangers/SKILL.md +17 -0
- package/catalog/skills/skill-creator/SKILL.md +34 -0
- package/catalog/skills/skill-creator/assets/SKILL-TEMPLATE.md +6 -0
- package/catalog/skills/spec-driven-development/SKILL.md +19 -0
- package/catalog/skills/test-driven-development/SKILL.md +17 -0
- package/catalog/skills/token-optimizer/SKILL.md +18 -0
- package/package.json +6 -4
- package/packages/catalog/package.json +1 -1
- package/packages/catalog/src/index.js +400 -4
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/bin.js +126 -161
- package/packages/cli/src/ink-ui.js +692 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/terminal.js +16 -5
- package/packages/core/src/ui/layout.js +193 -42
- package/packages/detectors/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cp, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
@@ -21,6 +21,16 @@ const REQUIRED_FIELDS = [
|
|
|
21
21
|
"dependencies"
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
+
const MIRRORED_SKILL_METADATA_KEYS = [
|
|
25
|
+
"author",
|
|
26
|
+
"last-edit",
|
|
27
|
+
"license",
|
|
28
|
+
"version",
|
|
29
|
+
"changelog",
|
|
30
|
+
"auto-invoke",
|
|
31
|
+
"allowed-tools"
|
|
32
|
+
];
|
|
33
|
+
|
|
24
34
|
export function getCatalogRoot() {
|
|
25
35
|
return catalogDir;
|
|
26
36
|
}
|
|
@@ -70,9 +80,372 @@ export function validateSkillManifest(manifest) {
|
|
|
70
80
|
throw new Error(`Skill "${manifest.id}" must declare files`);
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
const hasSkillInstruction = manifest.files.some((file) => file.path === "SKILL.md" && file.kind === "instruction");
|
|
84
|
+
if (!hasSkillInstruction) {
|
|
85
|
+
throw new Error(`Skill "${manifest.id}" must include files entry for SKILL.md as instruction`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assertFrontmatterFields(manifest);
|
|
89
|
+
|
|
73
90
|
return true;
|
|
74
91
|
}
|
|
75
92
|
|
|
93
|
+
function toLf(text) {
|
|
94
|
+
return text.replaceAll("\r\n", "\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function splitLinesWithOffsets(text) {
|
|
98
|
+
const lines = [];
|
|
99
|
+
let start = 0;
|
|
100
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
101
|
+
if (text[index] === "\n") {
|
|
102
|
+
lines.push({
|
|
103
|
+
text: text.slice(start, index),
|
|
104
|
+
start,
|
|
105
|
+
end: index + 1
|
|
106
|
+
});
|
|
107
|
+
start = index + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (start < text.length) {
|
|
111
|
+
lines.push({
|
|
112
|
+
text: text.slice(start),
|
|
113
|
+
start,
|
|
114
|
+
end: text.length
|
|
115
|
+
});
|
|
116
|
+
} else if (text.length === 0) {
|
|
117
|
+
lines.push({ text: "", start: 0, end: 0 });
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function yamlQuote(value) {
|
|
123
|
+
return JSON.stringify(String(value));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertFrontmatterFields(manifest) {
|
|
127
|
+
if (!manifest || typeof manifest !== "object") {
|
|
128
|
+
throw new Error("Invalid manifest while building SKILL.md frontmatter");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof manifest.description !== "string" || manifest.description.length === 0) {
|
|
132
|
+
throw new Error(`Skill "${manifest.id}" is missing required manifest.description for frontmatter mirroring`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!manifest.skillMetadata || typeof manifest.skillMetadata !== "object") {
|
|
136
|
+
throw new Error(`Skill "${manifest.id}" is missing required manifest.skillMetadata for frontmatter mirroring`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const key of MIRRORED_SKILL_METADATA_KEYS) {
|
|
140
|
+
if (!(key in manifest.skillMetadata)) {
|
|
141
|
+
throw new Error(`Skill "${manifest.id}" is missing required skillMetadata.${key} for frontmatter mirroring`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const scalarKeys = ["author", "last-edit", "license", "version", "changelog", "auto-invoke"];
|
|
146
|
+
for (const key of scalarKeys) {
|
|
147
|
+
if (typeof manifest.skillMetadata[key] !== "string" || manifest.skillMetadata[key].trim().length === 0) {
|
|
148
|
+
throw new Error(`Skill "${manifest.id}" has invalid skillMetadata.${key}; expected a non-empty string`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(manifest.skillMetadata["allowed-tools"])) {
|
|
153
|
+
throw new Error(`Skill "${manifest.id}" must declare skillMetadata.allowed-tools as an array`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const tool of manifest.skillMetadata["allowed-tools"]) {
|
|
157
|
+
if (typeof tool !== "string" || tool.trim().length === 0) {
|
|
158
|
+
throw new Error(`Skill "${manifest.id}" has invalid skillMetadata.allowed-tools; expected non-empty strings`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function buildSkillFrontmatterPayload(manifest) {
|
|
164
|
+
assertFrontmatterFields(manifest);
|
|
165
|
+
return {
|
|
166
|
+
description: manifest.description,
|
|
167
|
+
skillMetadata: {
|
|
168
|
+
author: manifest.skillMetadata.author,
|
|
169
|
+
"last-edit": manifest.skillMetadata["last-edit"],
|
|
170
|
+
license: manifest.skillMetadata.license,
|
|
171
|
+
version: manifest.skillMetadata.version,
|
|
172
|
+
changelog: manifest.skillMetadata.changelog,
|
|
173
|
+
"auto-invoke": manifest.skillMetadata["auto-invoke"],
|
|
174
|
+
"allowed-tools": [...manifest.skillMetadata["allowed-tools"]]
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderSkillFrontmatterInner(payload) {
|
|
180
|
+
const lines = [
|
|
181
|
+
`description: ${yamlQuote(payload.description)}`,
|
|
182
|
+
"skillMetadata:",
|
|
183
|
+
` author: ${yamlQuote(payload.skillMetadata.author)}`,
|
|
184
|
+
` last-edit: ${yamlQuote(payload.skillMetadata["last-edit"])}`,
|
|
185
|
+
` license: ${yamlQuote(payload.skillMetadata.license)}`,
|
|
186
|
+
` version: ${yamlQuote(payload.skillMetadata.version)}`,
|
|
187
|
+
` changelog: ${yamlQuote(payload.skillMetadata.changelog)}`,
|
|
188
|
+
` auto-invoke: ${yamlQuote(payload.skillMetadata["auto-invoke"])}`,
|
|
189
|
+
" allowed-tools:"
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const tool of payload.skillMetadata["allowed-tools"]) {
|
|
193
|
+
lines.push(` - ${yamlQuote(tool)}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function renderSkillFrontmatter(manifest) {
|
|
200
|
+
const payload = buildSkillFrontmatterPayload(manifest);
|
|
201
|
+
return `---\n${renderSkillFrontmatterInner(payload)}\n---\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function splitSkillMarkdown(content) {
|
|
205
|
+
const normalized = toLf(content);
|
|
206
|
+
const source = normalized.startsWith("\uFEFF") ? normalized.slice(1) : normalized;
|
|
207
|
+
const lines = splitLinesWithOffsets(source);
|
|
208
|
+
const mirroredKeys = new Set([
|
|
209
|
+
"description",
|
|
210
|
+
"skillMetadata",
|
|
211
|
+
"author",
|
|
212
|
+
"last-edit",
|
|
213
|
+
"license",
|
|
214
|
+
"version",
|
|
215
|
+
"changelog",
|
|
216
|
+
"auto-invoke",
|
|
217
|
+
"allowed-tools"
|
|
218
|
+
]);
|
|
219
|
+
const isYamlLike = (line) => (
|
|
220
|
+
line.trim().length === 0 ||
|
|
221
|
+
/^\s*[A-Za-z0-9_-]+:(?:\s.*)?$/.test(line) ||
|
|
222
|
+
/^\s*-\s+.*$/.test(line)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
let firstNonBlankIndex = 0;
|
|
226
|
+
while (firstNonBlankIndex < lines.length && lines[firstNonBlankIndex].text.trim().length === 0) {
|
|
227
|
+
firstNonBlankIndex += 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (firstNonBlankIndex >= lines.length || lines[firstNonBlankIndex].text !== "---") {
|
|
231
|
+
return {
|
|
232
|
+
hasFrontmatter: false,
|
|
233
|
+
malformedFrontmatter: false,
|
|
234
|
+
frontmatter: null,
|
|
235
|
+
body: source
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const openLine = lines[firstNonBlankIndex];
|
|
240
|
+
let sawKeyValue = false;
|
|
241
|
+
let sawMirroredKey = false;
|
|
242
|
+
const detectedKeys = new Set();
|
|
243
|
+
|
|
244
|
+
for (let index = firstNonBlankIndex + 1; index < lines.length; index += 1) {
|
|
245
|
+
const line = lines[index].text;
|
|
246
|
+
const nextLine = index + 1 < lines.length ? lines[index + 1].text : null;
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
sawMirroredKey &&
|
|
250
|
+
line.trim().length === 0 &&
|
|
251
|
+
nextLine &&
|
|
252
|
+
/^(?:[-*+]\s+|\d+\.\s+|#{1,6}\s+|>\s+|```)/.test(nextLine)
|
|
253
|
+
) {
|
|
254
|
+
return {
|
|
255
|
+
hasFrontmatter: true,
|
|
256
|
+
malformedFrontmatter: true,
|
|
257
|
+
frontmatter: null,
|
|
258
|
+
body: source.slice(lines[index + 1].start)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (line === "---") {
|
|
263
|
+
if (!sawKeyValue || !sawMirroredKey) {
|
|
264
|
+
return {
|
|
265
|
+
hasFrontmatter: false,
|
|
266
|
+
malformedFrontmatter: false,
|
|
267
|
+
frontmatter: null,
|
|
268
|
+
body: source
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const end = lines[index].end;
|
|
272
|
+
const frontmatter = source.slice(openLine.start, end);
|
|
273
|
+
return {
|
|
274
|
+
hasFrontmatter: true,
|
|
275
|
+
malformedFrontmatter: false,
|
|
276
|
+
frontmatter: frontmatter.endsWith("\n") ? frontmatter : `${frontmatter}\n`,
|
|
277
|
+
body: source.slice(end),
|
|
278
|
+
detectedKeys: Array.from(detectedKeys)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (/^\s*[A-Za-z0-9_-]+:(?:\s.*)?$/.test(line)) {
|
|
283
|
+
sawKeyValue = true;
|
|
284
|
+
const key = line.split(":", 1)[0].trim();
|
|
285
|
+
detectedKeys.add(key);
|
|
286
|
+
if (mirroredKeys.has(key)) {
|
|
287
|
+
sawMirroredKey = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!isYamlLike(line)) {
|
|
292
|
+
if (!sawKeyValue || !sawMirroredKey) {
|
|
293
|
+
return {
|
|
294
|
+
hasFrontmatter: false,
|
|
295
|
+
malformedFrontmatter: false,
|
|
296
|
+
frontmatter: null,
|
|
297
|
+
body: source
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
hasFrontmatter: true,
|
|
302
|
+
malformedFrontmatter: true,
|
|
303
|
+
frontmatter: null,
|
|
304
|
+
body: source.slice(lines[index].start),
|
|
305
|
+
detectedKeys: Array.from(detectedKeys)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!sawKeyValue) {
|
|
311
|
+
return {
|
|
312
|
+
hasFrontmatter: false,
|
|
313
|
+
malformedFrontmatter: false,
|
|
314
|
+
frontmatter: null,
|
|
315
|
+
body: source
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!sawMirroredKey) {
|
|
320
|
+
return {
|
|
321
|
+
hasFrontmatter: false,
|
|
322
|
+
malformedFrontmatter: false,
|
|
323
|
+
frontmatter: null,
|
|
324
|
+
body: source
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
hasFrontmatter: true,
|
|
330
|
+
malformedFrontmatter: true,
|
|
331
|
+
frontmatter: null,
|
|
332
|
+
body: "",
|
|
333
|
+
detectedKeys: Array.from(detectedKeys)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function applyManifestFrontmatterToSkill(content, manifest) {
|
|
338
|
+
const expectedFrontmatter = renderSkillFrontmatter(manifest);
|
|
339
|
+
const parts = splitSkillMarkdown(content);
|
|
340
|
+
return `${expectedFrontmatter}${parts.body}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function verifySkillFrontmatterContent(content, manifest) {
|
|
344
|
+
const expectedFrontmatter = renderSkillFrontmatter(manifest);
|
|
345
|
+
const parts = splitSkillMarkdown(content);
|
|
346
|
+
if (!parts.hasFrontmatter) {
|
|
347
|
+
return { ok: false, reason: "missing" };
|
|
348
|
+
}
|
|
349
|
+
if (parts.malformedFrontmatter || !parts.frontmatter) {
|
|
350
|
+
return { ok: false, reason: "malformed" };
|
|
351
|
+
}
|
|
352
|
+
if (parts.frontmatter !== expectedFrontmatter) {
|
|
353
|
+
return { ok: false, reason: "mismatch" };
|
|
354
|
+
}
|
|
355
|
+
const residual = splitSkillMarkdown(parts.body);
|
|
356
|
+
if (residual.hasFrontmatter && !residual.malformedFrontmatter && residual.frontmatter === expectedFrontmatter) {
|
|
357
|
+
return { ok: false, reason: "residual-frontmatter" };
|
|
358
|
+
}
|
|
359
|
+
return { ok: true, reason: null };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function syncSkillFrontmatter({ skillId, dryRun = false } = {}) {
|
|
363
|
+
const plan = await planSkillFrontmatterSync({ skillId });
|
|
364
|
+
if (!dryRun) {
|
|
365
|
+
await applyTextUpdatesAtomically(plan.updates);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
skillCount: plan.skillCount,
|
|
369
|
+
updatedSkillIds: plan.updatedSkillIds
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function planSkillFrontmatterSync({ skillId } = {}) {
|
|
374
|
+
const allIds = await listSkillIds();
|
|
375
|
+
if (skillId && !allIds.includes(skillId)) {
|
|
376
|
+
throw new Error(`Unknown skill id: ${skillId}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const ids = skillId ? [skillId] : allIds;
|
|
380
|
+
const updatedSkillIds = [];
|
|
381
|
+
const updates = [];
|
|
382
|
+
|
|
383
|
+
for (const id of ids) {
|
|
384
|
+
const manifest = await loadSkillManifest(id);
|
|
385
|
+
const skillPath = path.join(skillsDir, id, "SKILL.md");
|
|
386
|
+
const current = await readFile(skillPath, "utf8");
|
|
387
|
+
const next = applyManifestFrontmatterToSkill(current, manifest);
|
|
388
|
+
if (next !== toLf(current)) {
|
|
389
|
+
updatedSkillIds.push(id);
|
|
390
|
+
updates.push({
|
|
391
|
+
skillId: id,
|
|
392
|
+
path: skillPath,
|
|
393
|
+
content: next
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
skillCount: ids.length,
|
|
400
|
+
updatedSkillIds,
|
|
401
|
+
updates
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function applyTextUpdatesAtomically(updates) {
|
|
406
|
+
const deduped = [];
|
|
407
|
+
const seenPaths = new Set();
|
|
408
|
+
for (let index = updates.length - 1; index >= 0; index -= 1) {
|
|
409
|
+
const update = updates[index];
|
|
410
|
+
if (!seenPaths.has(update.path)) {
|
|
411
|
+
seenPaths.add(update.path);
|
|
412
|
+
deduped.push(update);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
deduped.reverse();
|
|
416
|
+
|
|
417
|
+
const originals = new Map();
|
|
418
|
+
for (const update of deduped) {
|
|
419
|
+
try {
|
|
420
|
+
originals.set(update.path, await readFile(update.path, "utf8"));
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error?.code === "ENOENT") {
|
|
423
|
+
originals.set(update.path, null);
|
|
424
|
+
} else {
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const writtenPaths = [];
|
|
431
|
+
try {
|
|
432
|
+
for (const update of deduped) {
|
|
433
|
+
await writeFile(update.path, update.content, "utf8");
|
|
434
|
+
writtenPaths.push(update.path);
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
for (const targetPath of writtenPaths.reverse()) {
|
|
438
|
+
const original = originals.get(targetPath);
|
|
439
|
+
if (original === null) {
|
|
440
|
+
await rm(targetPath, { force: true });
|
|
441
|
+
} else {
|
|
442
|
+
await writeFile(targetPath, original, "utf8");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
76
449
|
export async function copySkillTo(targetCatalogDir, skillId) {
|
|
77
450
|
const source = path.join(skillsDir, skillId);
|
|
78
451
|
const destination = path.join(targetCatalogDir, skillId);
|
|
@@ -88,6 +461,7 @@ export async function readTemplate(templateName) {
|
|
|
88
461
|
}
|
|
89
462
|
|
|
90
463
|
export function renderAgentsMarkdown({ skills, detections, generatedAt, projectName }) {
|
|
464
|
+
const escapeTableCell = (value) => String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
|
|
91
465
|
const sortedSkills = [...skills].sort((a, b) => a.id.localeCompare(b.id));
|
|
92
466
|
const sortedDetections = [...detections].sort((a, b) => a.technology.localeCompare(b.technology));
|
|
93
467
|
const autoInvokeSkills = sortedSkills.filter((skill) => skill.skillMetadata?.["auto-invoke"]);
|
|
@@ -114,7 +488,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
|
|
|
114
488
|
];
|
|
115
489
|
|
|
116
490
|
for (const skill of sortedSkills) {
|
|
117
|
-
lines.push(`| \`${skill.id}\` | ${skill.description} | ${skill.tags.join(", ")} |`);
|
|
491
|
+
lines.push(`| \`${skill.id}\` | ${escapeTableCell(skill.description)} | ${escapeTableCell(skill.tags.join(", "))} |`);
|
|
118
492
|
}
|
|
119
493
|
|
|
120
494
|
lines.push(
|
|
@@ -126,7 +500,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
|
|
|
126
500
|
"1. Always run `token-optimizer` first to classify complexity and set the minimum viable reasoning depth.",
|
|
127
501
|
"2. Always run `output-optimizer` immediately after `token-optimizer` for response-shape control.",
|
|
128
502
|
"3. `output-optimizer` mode policy:",
|
|
129
|
-
" - Default:
|
|
503
|
+
" - Default: use `step-brief` when there is no explicit mode or strong phrasing signal.",
|
|
130
504
|
" - Override: if user explicitly requests a mode (for example `mode: step-brief`), that explicit mode wins.",
|
|
131
505
|
" - Persistence: keep the explicitly requested mode active until the user asks for a different mode.",
|
|
132
506
|
"",
|
|
@@ -154,7 +528,7 @@ export function renderAgentsMarkdown({ skills, detections, generatedAt, projectN
|
|
|
154
528
|
} else {
|
|
155
529
|
lines.push("| Action | Skill |", "| ------ | ----- |");
|
|
156
530
|
for (const skill of autoInvokeSkills) {
|
|
157
|
-
lines.push(`| ${skill.skillMetadata["auto-invoke"]} | \`${skill.id}\` |`);
|
|
531
|
+
lines.push(`| ${escapeTableCell(skill.skillMetadata["auto-invoke"])} | \`${skill.id}\` |`);
|
|
158
532
|
}
|
|
159
533
|
}
|
|
160
534
|
|
|
@@ -230,6 +604,28 @@ export async function verifyCatalogFiles() {
|
|
|
230
604
|
issues.push(`Missing file for ${skillId}: ${file.path}`);
|
|
231
605
|
}
|
|
232
606
|
}
|
|
607
|
+
|
|
608
|
+
const skillDocPath = path.join(skillPath, "SKILL.md");
|
|
609
|
+
let skillDocContent;
|
|
610
|
+
try {
|
|
611
|
+
skillDocContent = await readFile(skillDocPath, "utf8");
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (error?.code === "ENOENT") {
|
|
614
|
+
// Missing file is already surfaced above via manifest file verification.
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
issues.push(`Cannot read ${skillId}/SKILL.md: ${error.message}`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const status = verifySkillFrontmatterContent(skillDocContent, manifest);
|
|
623
|
+
if (!status.ok) {
|
|
624
|
+
issues.push(`Frontmatter ${status.reason} for ${skillId}: SKILL.md`);
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
issues.push(`Frontmatter validation failed for ${skillId}: ${error.message}`);
|
|
628
|
+
}
|
|
233
629
|
}
|
|
234
630
|
|
|
235
631
|
return issues;
|