@amsterdamdatalabs/enact-extensions 0.1.47 → 0.1.49
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/bundled-extensions.json +1 -4
- package/dist/create/claude.d.ts.map +1 -1
- package/dist/create/claude.js +1 -1
- package/dist/create/claude.js.map +1 -1
- package/dist/create/codex.d.ts.map +1 -1
- package/dist/create/codex.js +2 -3
- package/dist/create/codex.js.map +1 -1
- package/dist/create/cursor.d.ts.map +1 -1
- package/dist/create/cursor.js +3 -7
- package/dist/create/cursor.js.map +1 -1
- package/dist/create/enact.d.ts.map +1 -1
- package/dist/create/enact.js.map +1 -1
- package/dist/create/index.d.ts +3 -3
- package/dist/create/index.d.ts.map +1 -1
- package/dist/create/index.js +4 -4
- package/dist/create/index.js.map +1 -1
- package/dist/create/kimi.d.ts.map +1 -1
- package/dist/create/kimi.js +2 -3
- package/dist/create/kimi.js.map +1 -1
- package/dist/create/opencode.d.ts.map +1 -1
- package/dist/create/opencode.js +5 -12
- package/dist/create/opencode.js.map +1 -1
- package/dist/index.d.ts +12 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/internal/agents.d.ts.map +1 -1
- package/dist/internal/agents.js +1 -1
- package/dist/internal/agents.js.map +1 -1
- package/dist/internal/extensions-table.d.ts +27 -6
- package/dist/internal/extensions-table.d.ts.map +1 -1
- package/dist/internal/extensions-table.js +85 -12
- package/dist/internal/extensions-table.js.map +1 -1
- package/dist/internal/file-ops.js +1 -1
- package/dist/internal/file-ops.js.map +1 -1
- package/dist/internal/io.d.ts.map +1 -1
- package/dist/internal/io.js.map +1 -1
- package/dist/internal/json-merge.d.ts.map +1 -1
- package/dist/internal/json-merge.js +4 -2
- package/dist/internal/json-merge.js.map +1 -1
- package/dist/internal/lock-ownership.d.ts.map +1 -1
- package/dist/internal/lock-ownership.js.map +1 -1
- package/dist/internal/pick.d.ts.map +1 -1
- package/dist/internal/pick.js.map +1 -1
- package/dist/internal/platform.d.ts.map +1 -1
- package/dist/internal/platform.js +7 -53
- package/dist/internal/platform.js.map +1 -1
- package/dist/internal/schema.d.ts.map +1 -1
- package/dist/internal/schema.js.map +1 -1
- package/dist/internal/toml-block.d.ts.map +1 -1
- package/dist/internal/toml-block.js.map +1 -1
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +2 -2
- package/dist/projection.js.map +1 -1
- package/dist/validate/index.d.ts +2 -2
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +3 -3
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +2 -5
- package/dist/validate/run.js.map +1 -1
- package/extensions/enact-repo/.agents/plugin.json +2 -2
- package/extensions/enact-repo/skills/interaction-design/references/animation-libraries-gsap.md +0 -1
- package/extensions/enact-repo/skills/interaction-design/references/animation-libraries.md +0 -1
- package/extensions/enact-repo/skills/interaction-design/references/scroll-animations.md +0 -1
- package/package.json +9 -8
- package/scripts/enact-extensions.mjs +331 -303
- package/scripts/lib/build-index.mjs +1 -3
- package/scripts/lib/resolve-bundle.mjs +1 -3
- package/scripts/lib/run-repo-doctor.mjs +50 -17
- package/scripts/lib/run-repo-install.mjs +130 -58
- package/scripts/lib/run-sync.mjs +3 -9
- package/scripts/lib/run-validate.mjs +1 -3
- package/scripts/lib/skill-validator.mjs +319 -95
|
@@ -29,15 +29,9 @@ export const REQUIRED_BOUNDARY_HEADINGS = ["In scope", "Out of scope", "Fog of w
|
|
|
29
29
|
* in-plugin route reachability, resource layout, and slop detection.
|
|
30
30
|
*/
|
|
31
31
|
export const FREEFORM_SECTION_PLUGINS = new Set(["engineering"]);
|
|
32
|
-
export const CANONICAL_FACTORY_STORAGE =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const ALLOWED_FRONTMATTER_KEYS = new Set([
|
|
36
|
-
"name",
|
|
37
|
-
"description",
|
|
38
|
-
"disable-model-invocation",
|
|
39
|
-
"compatibility",
|
|
40
|
-
]);
|
|
32
|
+
export const CANONICAL_FACTORY_STORAGE = "Factory artifacts go under `~/.enact/factory/projects/<key>/…`.";
|
|
33
|
+
|
|
34
|
+
const ALLOWED_FRONTMATTER_KEYS = new Set(["name", "description", "disable-model-invocation", "compatibility"]);
|
|
41
35
|
const COMPONENT_FIELDS = [
|
|
42
36
|
"skills",
|
|
43
37
|
"agents",
|
|
@@ -53,13 +47,20 @@ const COMPONENT_FIELDS = [
|
|
|
53
47
|
"rules",
|
|
54
48
|
"_agents",
|
|
55
49
|
];
|
|
56
|
-
const COMMAND_SHAPED_LINE =
|
|
50
|
+
const COMMAND_SHAPED_LINE =
|
|
51
|
+
/`[^`]*(\b(npm|git)\s+(run|test|start|build|install|ci|status|diff|log|show|stash|commit|push|pull|checkout|merge-base|rev-parse)\b|\b(node|npx|paseo|cargo|az|tsc|grep)\s+\S|loop_[a-z]+|factory_[a-z]+|exit 0)[^`]*`/i;
|
|
57
52
|
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{26FF}\u{2705}\u{274C}\u{2713}-\u{2716}]/u;
|
|
58
53
|
const SLOP_PATTERNS = [
|
|
59
54
|
["slop-storage-banner", /Storage is GLOBAL|never write to a repo-local `?\.enact/i],
|
|
60
55
|
["slop-workspace-recipe", /~\/\.enact\/workspaces\.json|Resolve the project'?s global dir/i],
|
|
61
|
-
[
|
|
62
|
-
|
|
56
|
+
[
|
|
57
|
+
"slop-marketing",
|
|
58
|
+
/\b(seamless|best practices|dive in|game[- ]changer|supercharge|effortless|cutting-edge|comprehensive|powerful|leverage(?:d|s|ing)?|elegant|closes? the gap|unlock|elevate|world-class|next-level|revolutionary|synerg(?:y|ies|ize)|utilize)\b/i,
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"slop-filler",
|
|
62
|
+
/\b(it'?s worth noting|it'?s important to note|note that|in summary|in conclusion|obviously|clearly)\b/i,
|
|
63
|
+
],
|
|
63
64
|
["slop-self-reference", /\b(generated by an AI|LLM-authored|as an AI)\b/i],
|
|
64
65
|
["slop-edit-scar", /as of \)|(?<![A-Za-z0-9_])\(\s*\)|\(,\s|^#{1,6}\s+.* —\s*$/im],
|
|
65
66
|
];
|
|
@@ -193,7 +194,9 @@ function frontmatter(text, path, diagnostics) {
|
|
|
193
194
|
const lines = text.split(/\r?\n/);
|
|
194
195
|
const closing = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
195
196
|
if (closing === -1) {
|
|
196
|
-
diagnostics.push(
|
|
197
|
+
diagnostics.push(
|
|
198
|
+
diagnostic("frontmatter-unterminated", path, "YAML frontmatter must end with a closing --- line."),
|
|
199
|
+
);
|
|
197
200
|
return { metadata: null, body: text };
|
|
198
201
|
}
|
|
199
202
|
const yaml = lines.slice(1, closing).join("\n");
|
|
@@ -265,7 +268,9 @@ function resourceReferences(text, knownResources) {
|
|
|
265
268
|
if (candidate.startsWith("scripts/") && /(?:node|npx)\s+$/i.test(prefix)) continue;
|
|
266
269
|
references.add(candidate);
|
|
267
270
|
}
|
|
268
|
-
for (const match of text.matchAll(
|
|
271
|
+
for (const match of text.matchAll(
|
|
272
|
+
/\[[^\]]*]\((?:\.\/)?((?:scripts|references|assets)\/[A-Za-z0-9._/-]+)(?:#[^)\s]+)?\)/g,
|
|
273
|
+
)) {
|
|
269
274
|
const candidate = match[1].replace(/[),.;:]+$/, "");
|
|
270
275
|
if (!candidate.endsWith("/") && !candidate.endsWith(".")) references.add(candidate);
|
|
271
276
|
}
|
|
@@ -286,15 +291,18 @@ function textResource(path) {
|
|
|
286
291
|
|
|
287
292
|
function validateScript(path, relativePath, diagnostics) {
|
|
288
293
|
const extension = extname(path).toLowerCase();
|
|
289
|
-
const command =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
const command =
|
|
295
|
+
extension === ".py"
|
|
296
|
+
? ["python3", ["-c", "import ast,pathlib,sys; ast.parse(pathlib.Path(sys.argv[1]).read_text())", path]]
|
|
297
|
+
: extension === ".sh"
|
|
298
|
+
? ["bash", ["-n", path]]
|
|
299
|
+
: new Set([".js", ".mjs", ".cjs"]).has(extension)
|
|
300
|
+
? [process.execPath, ["--check", path]]
|
|
301
|
+
: null;
|
|
296
302
|
if (!command) {
|
|
297
|
-
diagnostics.push(
|
|
303
|
+
diagnostics.push(
|
|
304
|
+
diagnostic("script-extension", relativePath, `Unsupported script extension ${extension || "(none)"}.`),
|
|
305
|
+
);
|
|
298
306
|
return;
|
|
299
307
|
}
|
|
300
308
|
try {
|
|
@@ -308,7 +316,14 @@ function validateScript(path, relativePath, diagnostics) {
|
|
|
308
316
|
function normalizedParagraphs(body) {
|
|
309
317
|
return withoutFencedCode(body)
|
|
310
318
|
.split(/\n\s*\n/)
|
|
311
|
-
.map((part) =>
|
|
319
|
+
.map((part) =>
|
|
320
|
+
part
|
|
321
|
+
.replace(/^#{1,6}\s+.*$/gm, "")
|
|
322
|
+
.replace(/[`*_>#-]/g, " ")
|
|
323
|
+
.replace(/\s+/g, " ")
|
|
324
|
+
.trim()
|
|
325
|
+
.toLowerCase(),
|
|
326
|
+
)
|
|
312
327
|
.filter((part) => part.length >= 120 && part !== CANONICAL_FACTORY_STORAGE.toLowerCase());
|
|
313
328
|
}
|
|
314
329
|
|
|
@@ -328,7 +343,13 @@ function pushDuplicateParagraphDiagnostics(paragraphsByText, diagnostics) {
|
|
|
328
343
|
const bundle = bundleOf(path);
|
|
329
344
|
const sameBundleOthers = paths.filter((other) => other !== path && bundleOf(other) === bundle);
|
|
330
345
|
if (sameBundleOthers.length === 0) continue;
|
|
331
|
-
diagnostics.push(
|
|
346
|
+
diagnostics.push(
|
|
347
|
+
diagnostic(
|
|
348
|
+
"slop-duplicate-paragraph",
|
|
349
|
+
path,
|
|
350
|
+
`Duplicate prose paragraph also appears in: ${sameBundleOthers.join(", ")}.`,
|
|
351
|
+
),
|
|
352
|
+
);
|
|
332
353
|
}
|
|
333
354
|
}
|
|
334
355
|
}
|
|
@@ -341,7 +362,8 @@ function validateSlop(text, path, diagnostics) {
|
|
|
341
362
|
if (/^\s*(?:[-*]\s+)?\*\*[^*\n]{60,}\*\*[.!?]?\s*$/m.test(prose)) {
|
|
342
363
|
diagnostics.push(diagnostic("slop-overbold", path, "Do not bold a whole sentence for emphasis."));
|
|
343
364
|
}
|
|
344
|
-
if (EMOJI.test(prose))
|
|
365
|
+
if (EMOJI.test(prose))
|
|
366
|
+
diagnostics.push(diagnostic("slop-emoji", path, "Emoji and decorative symbols are not allowed."));
|
|
345
367
|
}
|
|
346
368
|
|
|
347
369
|
/**
|
|
@@ -361,7 +383,9 @@ function validateSkillRoutes(skills, diagnostics) {
|
|
|
361
383
|
for (const match of skill.body.matchAll(SKILL_ROUTE_REFERENCE)) {
|
|
362
384
|
const target = match[1];
|
|
363
385
|
if (target.includes("-") && !names.has(target)) {
|
|
364
|
-
diagnostics.push(
|
|
386
|
+
diagnostics.push(
|
|
387
|
+
diagnostic("skill-route-missing", skill.path, `Referenced skill does not exist in this plugin: ${target}.`),
|
|
388
|
+
);
|
|
365
389
|
}
|
|
366
390
|
}
|
|
367
391
|
}
|
|
@@ -370,7 +394,13 @@ function validateSkillRoutes(skills, diagnostics) {
|
|
|
370
394
|
function validateCanonicalManifest(schemaRoot, manifest, pluginRelative, diagnostics) {
|
|
371
395
|
const schemaPath = join(schemaRoot, "spec", "enact.json");
|
|
372
396
|
if (!isFile(schemaPath)) {
|
|
373
|
-
diagnostics.push(
|
|
397
|
+
diagnostics.push(
|
|
398
|
+
diagnostic(
|
|
399
|
+
"manifest-schema",
|
|
400
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
401
|
+
"Canonical schema spec/enact.json is missing.",
|
|
402
|
+
),
|
|
403
|
+
);
|
|
374
404
|
return;
|
|
375
405
|
}
|
|
376
406
|
let validator = manifestSchemaValidators.get(schemaPath);
|
|
@@ -383,15 +413,23 @@ function validateCanonicalManifest(schemaRoot, manifest, pluginRelative, diagnos
|
|
|
383
413
|
}
|
|
384
414
|
if (!validator(manifest)) {
|
|
385
415
|
for (const error of validator.errors ?? []) {
|
|
386
|
-
diagnostics.push(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
diagnostics.push(
|
|
417
|
+
diagnostic(
|
|
418
|
+
"manifest-schema",
|
|
419
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
420
|
+
`${error.instancePath || "/"}: ${error.message ?? "invalid"}.`,
|
|
421
|
+
),
|
|
422
|
+
);
|
|
391
423
|
}
|
|
392
424
|
}
|
|
393
425
|
} catch (err) {
|
|
394
|
-
diagnostics.push(
|
|
426
|
+
diagnostics.push(
|
|
427
|
+
diagnostic(
|
|
428
|
+
"manifest-schema",
|
|
429
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
430
|
+
`Cannot validate canonical manifest: ${err instanceof Error ? err.message : String(err)}`,
|
|
431
|
+
),
|
|
432
|
+
);
|
|
395
433
|
}
|
|
396
434
|
}
|
|
397
435
|
|
|
@@ -413,18 +451,31 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
413
451
|
}
|
|
414
452
|
if (entry.isFile() && entry.name !== "SKILL.md") {
|
|
415
453
|
if (!SKILL_ROOT_ALLOWED_EXTRA_FILES.test(entry.name)) {
|
|
416
|
-
diagnostics.push(
|
|
454
|
+
diagnostics.push(
|
|
455
|
+
diagnostic(
|
|
456
|
+
"skill-root-file",
|
|
457
|
+
relativePath,
|
|
458
|
+
"Only SKILL.md, LICENSE, LICENSE.*, and version.json may be files directly under a skill root.",
|
|
459
|
+
),
|
|
460
|
+
);
|
|
417
461
|
}
|
|
418
462
|
continue;
|
|
419
463
|
}
|
|
420
464
|
if (entry.isDirectory() && !SKILL_ROOT_DIRECTORIES.has(entry.name)) {
|
|
421
|
-
diagnostics.push(
|
|
465
|
+
diagnostics.push(
|
|
466
|
+
diagnostic(
|
|
467
|
+
"skill-root-directory",
|
|
468
|
+
relativePath,
|
|
469
|
+
"Only scripts/, references/, and assets/ are allowed under a skill root.",
|
|
470
|
+
),
|
|
471
|
+
);
|
|
422
472
|
continue;
|
|
423
473
|
}
|
|
424
474
|
if (entry.isDirectory()) {
|
|
425
475
|
for (const file of listFiles(path)) {
|
|
426
476
|
const fullRelative = `${rootRelative}/${entry.name}/${file.relative}`;
|
|
427
|
-
if (file.symlink)
|
|
477
|
+
if (file.symlink)
|
|
478
|
+
diagnostics.push(diagnostic("skill-symlink", fullRelative, "Symlinks are not allowed in a skill."));
|
|
428
479
|
else resources.push({ ...file, relative: `${entry.name}/${file.relative}`, fullRelative });
|
|
429
480
|
}
|
|
430
481
|
}
|
|
@@ -432,36 +483,54 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
432
483
|
|
|
433
484
|
const skillPath = join(skillRoot, "SKILL.md");
|
|
434
485
|
if (!isFile(skillPath)) {
|
|
435
|
-
diagnostics.push(
|
|
486
|
+
diagnostics.push(
|
|
487
|
+
diagnostic("skill-missing", `${rootRelative}/SKILL.md`, "Every skill directory requires SKILL.md."),
|
|
488
|
+
);
|
|
436
489
|
return { diagnostics, skillName, path: `${rootRelative}/SKILL.md`, body: "", paragraphs: [] };
|
|
437
490
|
}
|
|
438
491
|
const text = readFileSync(skillPath, "utf8");
|
|
439
492
|
const skillRelative = `${rootRelative}/SKILL.md`;
|
|
440
493
|
const freeformSections = FREEFORM_SECTION_PLUGINS.has(pluginName);
|
|
441
494
|
if (!freeformSections && lineCount(text) > 100) {
|
|
442
|
-
diagnostics.push(
|
|
495
|
+
diagnostics.push(
|
|
496
|
+
diagnostic("skill-line-limit", skillRelative, `SKILL.md has ${lineCount(text)} lines; maximum is 100.`),
|
|
497
|
+
);
|
|
443
498
|
}
|
|
444
499
|
|
|
445
500
|
const parsed = frontmatter(text, skillRelative, diagnostics);
|
|
446
501
|
if (parsed.metadata) {
|
|
447
502
|
for (const key of Object.keys(parsed.metadata)) {
|
|
448
503
|
if (!ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
449
|
-
diagnostics.push(
|
|
504
|
+
diagnostics.push(
|
|
505
|
+
diagnostic("frontmatter-key", skillRelative, `Unexpected frontmatter key ${JSON.stringify(key)}.`),
|
|
506
|
+
);
|
|
450
507
|
}
|
|
451
508
|
}
|
|
452
509
|
if (parsed.metadata.name !== skillName) {
|
|
453
|
-
diagnostics.push(
|
|
510
|
+
diagnostics.push(
|
|
511
|
+
diagnostic("frontmatter-name", skillRelative, `name must equal directory name ${JSON.stringify(skillName)}.`),
|
|
512
|
+
);
|
|
454
513
|
}
|
|
455
514
|
if (typeof parsed.metadata.description !== "string" || !parsed.metadata.description.trim()) {
|
|
456
515
|
diagnostics.push(diagnostic("frontmatter-description", skillRelative, "description must be a non-empty string."));
|
|
457
516
|
} else if (/^this skill should be used when\b/i.test(parsed.metadata.description.trim())) {
|
|
458
|
-
diagnostics.push(
|
|
517
|
+
diagnostics.push(
|
|
518
|
+
diagnostic(
|
|
519
|
+
"frontmatter-description-slop",
|
|
520
|
+
skillRelative,
|
|
521
|
+
"Use a direct description; do not start with generic trigger boilerplate.",
|
|
522
|
+
),
|
|
523
|
+
);
|
|
459
524
|
}
|
|
460
525
|
if (typeof parsed.metadata["disable-model-invocation"] !== "boolean") {
|
|
461
|
-
diagnostics.push(
|
|
526
|
+
diagnostics.push(
|
|
527
|
+
diagnostic("frontmatter-invocation", skillRelative, "disable-model-invocation must be an explicit boolean."),
|
|
528
|
+
);
|
|
462
529
|
}
|
|
463
530
|
if (parsed.metadata.compatibility !== undefined && typeof parsed.metadata.compatibility !== "string") {
|
|
464
|
-
diagnostics.push(
|
|
531
|
+
diagnostics.push(
|
|
532
|
+
diagnostic("frontmatter-compatibility", skillRelative, "compatibility must be a string when present."),
|
|
533
|
+
);
|
|
465
534
|
}
|
|
466
535
|
}
|
|
467
536
|
|
|
@@ -478,11 +547,13 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
478
547
|
const useWhenIndex = actualH2.indexOf(useWhen);
|
|
479
548
|
const doNotUseWhenIndex = actualH2.indexOf(doNotUseWhen);
|
|
480
549
|
if (useWhenIndex === -1 || doNotUseWhenIndex === -1 || useWhenIndex > doNotUseWhenIndex) {
|
|
481
|
-
diagnostics.push(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
550
|
+
diagnostics.push(
|
|
551
|
+
diagnostic(
|
|
552
|
+
"heading-contract",
|
|
553
|
+
skillRelative,
|
|
554
|
+
`SKILL.md must contain H2 headings \`${useWhen}\` and \`${doNotUseWhen}\`, with \`${useWhen}\` before \`${doNotUseWhen}\`. Any other H2 headings are allowed, in any order.`,
|
|
555
|
+
),
|
|
556
|
+
);
|
|
486
557
|
}
|
|
487
558
|
}
|
|
488
559
|
for (const heading of h2) {
|
|
@@ -496,32 +567,57 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
496
567
|
.filter((entry) => entry.level === 3 && entry.line > boundaries.line && (!nextH2 || entry.line < nextH2.line))
|
|
497
568
|
.map((entry) => entry.text);
|
|
498
569
|
if (boundaryH3.join("\u0000") !== REQUIRED_BOUNDARY_HEADINGS.join("\u0000")) {
|
|
499
|
-
diagnostics.push(
|
|
570
|
+
diagnostics.push(
|
|
571
|
+
diagnostic(
|
|
572
|
+
"boundaries-contract",
|
|
573
|
+
skillRelative,
|
|
574
|
+
`## Boundaries must contain exact H3 headings: ${REQUIRED_BOUNDARY_HEADINGS.join(" → ")}.`,
|
|
575
|
+
),
|
|
576
|
+
);
|
|
500
577
|
}
|
|
501
578
|
}
|
|
502
579
|
const completion = h2.find((entry) => entry.text === "Completion");
|
|
503
580
|
if (completion && !COMMAND_SHAPED_LINE.test(sectionText(body, completion))) {
|
|
504
|
-
diagnostics.push(
|
|
581
|
+
diagnostics.push(
|
|
582
|
+
diagnostic("completion-proof", skillRelative, "## Completion requires a command-shaped proof line."),
|
|
583
|
+
);
|
|
505
584
|
}
|
|
506
585
|
|
|
507
586
|
const storageCount = body.split(CANONICAL_FACTORY_STORAGE).length - 1;
|
|
508
587
|
const factoryPathMentioned = /~\/\.enact\/factory\/projects\/[A-Za-z0-9_<>.-]+\//.test(body);
|
|
509
588
|
if (storageCount > 1) {
|
|
510
|
-
diagnostics.push(
|
|
589
|
+
diagnostics.push(
|
|
590
|
+
diagnostic("storage-duplicate", skillRelative, "The canonical Factory storage sentence may appear at most once."),
|
|
591
|
+
);
|
|
511
592
|
}
|
|
512
593
|
const h1Line = h1[0]?.line;
|
|
513
|
-
const firstLineAfterH1 =
|
|
514
|
-
|
|
515
|
-
|
|
594
|
+
const firstLineAfterH1 =
|
|
595
|
+
h1Line === undefined
|
|
596
|
+
? null
|
|
597
|
+
: (body
|
|
598
|
+
.split(/\r?\n/)
|
|
599
|
+
.slice(h1Line)
|
|
600
|
+
.find((line) => line.trim())
|
|
601
|
+
?.trim() ?? null);
|
|
516
602
|
if (factoryPathMentioned && (storageCount !== 1 || firstLineAfterH1 !== CANONICAL_FACTORY_STORAGE)) {
|
|
517
|
-
diagnostics.push(
|
|
603
|
+
diagnostics.push(
|
|
604
|
+
diagnostic(
|
|
605
|
+
"storage-contract",
|
|
606
|
+
skillRelative,
|
|
607
|
+
`Factory path users must place exactly this sentence immediately after the H1: ${CANONICAL_FACTORY_STORAGE}`,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
518
610
|
}
|
|
519
611
|
const noncanonicalStorage = withoutFencedCode(body)
|
|
520
612
|
.replaceAll(CANONICAL_FACTORY_STORAGE, "")
|
|
521
613
|
.replace(/`[^`]*`/g, "")
|
|
522
|
-
.match(
|
|
614
|
+
.match(
|
|
615
|
+
/~\/\.enact\/factory\/projects\/|\bfactory\s+(?:artifacts?|storage|workspace)\s+(?:are|is|go|live|reside|belong|located|stored)\b|\b(?:store|write|save)\b[^\n.]{0,100}\b~\/\.enact\b/i,
|
|
616
|
+
);
|
|
523
617
|
if (noncanonicalStorage) {
|
|
524
|
-
diagnostics.push(
|
|
618
|
+
diagnostics.push(
|
|
619
|
+
diagnostic("storage-noncanonical", skillRelative, "Use only the canonical Factory storage sentence."),
|
|
620
|
+
);
|
|
525
621
|
}
|
|
526
622
|
validateSlop(body, skillRelative, diagnostics);
|
|
527
623
|
|
|
@@ -538,7 +634,13 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
538
634
|
if (!resource) {
|
|
539
635
|
const key = `${current.relative}\u0000${reference}`;
|
|
540
636
|
if (!missingResources.has(key)) {
|
|
541
|
-
diagnostics.push(
|
|
637
|
+
diagnostics.push(
|
|
638
|
+
diagnostic(
|
|
639
|
+
"resource-missing",
|
|
640
|
+
current.fullRelative,
|
|
641
|
+
`Local resource reference does not exist: ${reference}.`,
|
|
642
|
+
),
|
|
643
|
+
);
|
|
542
644
|
missingResources.add(key);
|
|
543
645
|
}
|
|
544
646
|
continue;
|
|
@@ -551,13 +653,21 @@ function validateSkill(skillRoot, pluginName) {
|
|
|
551
653
|
for (const resource of resources) {
|
|
552
654
|
const resourceText = textResource(resource.absolute);
|
|
553
655
|
if (/\.md$/i.test(resource.relative) && resourceText !== null && lineCount(resourceText) > 300) {
|
|
554
|
-
diagnostics.push(
|
|
656
|
+
diagnostics.push(
|
|
657
|
+
diagnostic(
|
|
658
|
+
"resource-line-limit",
|
|
659
|
+
resource.fullRelative,
|
|
660
|
+
`Markdown resource has ${lineCount(resourceText)} lines; maximum is 300.`,
|
|
661
|
+
),
|
|
662
|
+
);
|
|
555
663
|
}
|
|
556
664
|
if (/\.md$/i.test(resource.relative) && resourceText !== null) {
|
|
557
665
|
validateSlop(resourceText, resource.fullRelative, diagnostics);
|
|
558
666
|
}
|
|
559
667
|
if (!reachable.has(resource.relative)) {
|
|
560
|
-
diagnostics.push(
|
|
668
|
+
diagnostics.push(
|
|
669
|
+
diagnostic("resource-orphan", resource.fullRelative, "Resource is not reachable from SKILL.md."),
|
|
670
|
+
);
|
|
561
671
|
}
|
|
562
672
|
if (resource.relative.startsWith("scripts/")) validateScript(resource.absolute, resource.fullRelative, diagnostics);
|
|
563
673
|
}
|
|
@@ -580,11 +690,23 @@ function manifestPaths(pluginRoot, manifest, pluginRelative, diagnostics) {
|
|
|
580
690
|
if (raw === undefined) continue;
|
|
581
691
|
const resolved = safeRelative(pluginRoot, raw);
|
|
582
692
|
if (!resolved) {
|
|
583
|
-
diagnostics.push(
|
|
693
|
+
diagnostics.push(
|
|
694
|
+
diagnostic(
|
|
695
|
+
"manifest-path",
|
|
696
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
697
|
+
`${field} must be a safe plugin-relative path.`,
|
|
698
|
+
),
|
|
699
|
+
);
|
|
584
700
|
continue;
|
|
585
701
|
}
|
|
586
702
|
if (!existsSync(resolved.absolute)) {
|
|
587
|
-
diagnostics.push(
|
|
703
|
+
diagnostics.push(
|
|
704
|
+
diagnostic(
|
|
705
|
+
"manifest-path-missing",
|
|
706
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
707
|
+
`${field} path does not exist: ${raw}`,
|
|
708
|
+
),
|
|
709
|
+
);
|
|
588
710
|
continue;
|
|
589
711
|
}
|
|
590
712
|
paths.push(resolved.relative);
|
|
@@ -596,7 +718,13 @@ function manifestPaths(pluginRoot, manifest, pluginRelative, diagnostics) {
|
|
|
596
718
|
if (raw === undefined) continue;
|
|
597
719
|
const resolved = safeRelative(pluginRoot, raw);
|
|
598
720
|
if (!resolved || !isFile(resolved.absolute)) {
|
|
599
|
-
diagnostics.push(
|
|
721
|
+
diagnostics.push(
|
|
722
|
+
diagnostic(
|
|
723
|
+
"manifest-asset",
|
|
724
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
725
|
+
`${field} must reference an existing plugin-relative file.`,
|
|
726
|
+
),
|
|
727
|
+
);
|
|
600
728
|
} else {
|
|
601
729
|
paths.push(resolved.relative);
|
|
602
730
|
}
|
|
@@ -604,12 +732,24 @@ function manifestPaths(pluginRoot, manifest, pluginRelative, diagnostics) {
|
|
|
604
732
|
}
|
|
605
733
|
if (manifest.assets !== undefined) {
|
|
606
734
|
if (!Array.isArray(manifest.assets)) {
|
|
607
|
-
diagnostics.push(
|
|
735
|
+
diagnostics.push(
|
|
736
|
+
diagnostic(
|
|
737
|
+
"assets-path",
|
|
738
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
739
|
+
"assets must be an array of plugin-relative paths.",
|
|
740
|
+
),
|
|
741
|
+
);
|
|
608
742
|
} else {
|
|
609
743
|
for (const raw of manifest.assets) {
|
|
610
744
|
const resolved = safeRelative(pluginRoot, raw);
|
|
611
745
|
if (!resolved || !existsSync(resolved.absolute)) {
|
|
612
|
-
diagnostics.push(
|
|
746
|
+
diagnostics.push(
|
|
747
|
+
diagnostic(
|
|
748
|
+
"assets-path",
|
|
749
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
750
|
+
`assets entry must be a safe, existing plugin-relative path: ${JSON.stringify(raw)}`,
|
|
751
|
+
),
|
|
752
|
+
);
|
|
613
753
|
} else {
|
|
614
754
|
paths.push(resolved.relative);
|
|
615
755
|
}
|
|
@@ -619,34 +759,66 @@ function manifestPaths(pluginRoot, manifest, pluginRelative, diagnostics) {
|
|
|
619
759
|
if (manifest.principles !== undefined) {
|
|
620
760
|
const resolved = safeRelative(pluginRoot, manifest.principles);
|
|
621
761
|
if (!resolved || !isFile(resolved.absolute)) {
|
|
622
|
-
diagnostics.push(
|
|
762
|
+
diagnostics.push(
|
|
763
|
+
diagnostic(
|
|
764
|
+
"principles-config",
|
|
765
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
766
|
+
"principles must reference an existing plugin-relative config file.",
|
|
767
|
+
),
|
|
768
|
+
);
|
|
623
769
|
} else {
|
|
624
770
|
paths.push(resolved.relative);
|
|
625
771
|
try {
|
|
626
772
|
const config = JSON.parse(readFileSync(resolved.absolute, "utf8"));
|
|
627
773
|
if (!Array.isArray(config.fragments)) {
|
|
628
|
-
diagnostics.push(
|
|
774
|
+
diagnostics.push(
|
|
775
|
+
diagnostic(
|
|
776
|
+
"principles-config",
|
|
777
|
+
`${pluginRelative}/${resolved.relative}`,
|
|
778
|
+
"Prompt config must declare a fragments array.",
|
|
779
|
+
),
|
|
780
|
+
);
|
|
629
781
|
} else {
|
|
630
782
|
for (const fragment of config.fragments) {
|
|
631
783
|
const fragmentPath = safeRelative(dirname(resolved.absolute), fragment?.file);
|
|
632
784
|
if (!fragmentPath || !isFile(fragmentPath.absolute) || !isWithin(pluginRoot, fragmentPath.absolute)) {
|
|
633
|
-
diagnostics.push(
|
|
785
|
+
diagnostics.push(
|
|
786
|
+
diagnostic(
|
|
787
|
+
"principles-fragment",
|
|
788
|
+
`${pluginRelative}/${resolved.relative}`,
|
|
789
|
+
"Each prompt fragment must reference an existing file inside the plugin.",
|
|
790
|
+
),
|
|
791
|
+
);
|
|
634
792
|
} else {
|
|
635
793
|
paths.push(toPosix(relative(pluginRoot, fragmentPath.absolute)));
|
|
636
794
|
}
|
|
637
795
|
}
|
|
638
796
|
}
|
|
639
797
|
} catch (err) {
|
|
640
|
-
diagnostics.push(
|
|
798
|
+
diagnostics.push(
|
|
799
|
+
diagnostic(
|
|
800
|
+
"principles-config",
|
|
801
|
+
`${pluginRelative}/${resolved.relative}`,
|
|
802
|
+
`Cannot parse prompt config: ${err instanceof Error ? err.message : String(err)}`,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
641
805
|
}
|
|
642
806
|
}
|
|
643
807
|
}
|
|
644
808
|
if (typeof manifest.validate === "string") {
|
|
645
|
-
const localScript = /(?:^|\s)(?:node\s+)?(?:\.\/)?([A-Za-z0-9_./-]+\.(?:mjs|cjs|js|py|sh))(?:\s|$)/.exec(
|
|
809
|
+
const localScript = /(?:^|\s)(?:node\s+)?(?:\.\/)?([A-Za-z0-9_./-]+\.(?:mjs|cjs|js|py|sh))(?:\s|$)/.exec(
|
|
810
|
+
manifest.validate,
|
|
811
|
+
);
|
|
646
812
|
if (localScript) {
|
|
647
813
|
const resolved = safeRelative(pluginRoot, localScript[1]);
|
|
648
814
|
if (!resolved || !isFile(resolved.absolute)) {
|
|
649
|
-
diagnostics.push(
|
|
815
|
+
diagnostics.push(
|
|
816
|
+
diagnostic(
|
|
817
|
+
"manifest-validate-script",
|
|
818
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
819
|
+
`validate references a missing local script: ${localScript[1]}`,
|
|
820
|
+
),
|
|
821
|
+
);
|
|
650
822
|
} else {
|
|
651
823
|
paths.push(resolved.relative);
|
|
652
824
|
}
|
|
@@ -660,29 +832,56 @@ function validatePluginRoot(pluginRoot, pluginName, tracked, diagnostics, schema
|
|
|
660
832
|
const pluginRelative = `extensions/${pluginName}`;
|
|
661
833
|
const manifestPath = join(pluginRoot, ".agents", "plugin.json");
|
|
662
834
|
if (!isFile(manifestPath)) {
|
|
663
|
-
diagnostics.push(
|
|
835
|
+
diagnostics.push(
|
|
836
|
+
diagnostic(
|
|
837
|
+
"manifest-missing",
|
|
838
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
839
|
+
"Plugin requires a canonical .agents/plugin.json.",
|
|
840
|
+
),
|
|
841
|
+
);
|
|
664
842
|
return { skills: [] };
|
|
665
843
|
}
|
|
666
844
|
let manifest;
|
|
667
845
|
try {
|
|
668
846
|
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
669
847
|
} catch (err) {
|
|
670
|
-
diagnostics.push(
|
|
848
|
+
diagnostics.push(
|
|
849
|
+
diagnostic(
|
|
850
|
+
"manifest-invalid-json",
|
|
851
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
852
|
+
err instanceof Error ? err.message : String(err),
|
|
853
|
+
),
|
|
854
|
+
);
|
|
671
855
|
return { skills: [] };
|
|
672
856
|
}
|
|
673
857
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
674
|
-
diagnostics.push(
|
|
858
|
+
diagnostics.push(
|
|
859
|
+
diagnostic("manifest-shape", `${pluginRelative}/.agents/plugin.json`, "Plugin manifest must be an object."),
|
|
860
|
+
);
|
|
675
861
|
return { skills: [] };
|
|
676
862
|
}
|
|
677
863
|
validateCanonicalManifest(schemaRoot, manifest, pluginRelative, diagnostics);
|
|
678
|
-
const requiredTargets =
|
|
679
|
-
|
|
680
|
-
|
|
864
|
+
const requiredTargets =
|
|
865
|
+
Array.isArray(manifest.targets) && manifest.targets.at(-1) === OPT_IN_TARGET
|
|
866
|
+
? manifest.targets.slice(0, -1)
|
|
867
|
+
: manifest.targets;
|
|
681
868
|
if (!Array.isArray(requiredTargets) || requiredTargets.join("\u0000") !== ALL_SURFACE_TARGETS.join("\u0000")) {
|
|
682
|
-
diagnostics.push(
|
|
869
|
+
diagnostics.push(
|
|
870
|
+
diagnostic(
|
|
871
|
+
"manifest-targets",
|
|
872
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
873
|
+
`targets must be exactly ${JSON.stringify(ALL_SURFACE_TARGETS)}, optionally followed by "${OPT_IN_TARGET}".`,
|
|
874
|
+
),
|
|
875
|
+
);
|
|
683
876
|
}
|
|
684
877
|
if (typeof manifest.version !== "string" || !/^\d+\.\d+\.\d+$/.test(manifest.version)) {
|
|
685
|
-
diagnostics.push(
|
|
878
|
+
diagnostics.push(
|
|
879
|
+
diagnostic(
|
|
880
|
+
"manifest-version",
|
|
881
|
+
`${pluginRelative}/.agents/plugin.json`,
|
|
882
|
+
"version must be semantic MAJOR.MINOR.PATCH.",
|
|
883
|
+
),
|
|
884
|
+
);
|
|
686
885
|
}
|
|
687
886
|
const allowed = manifestPaths(pluginRoot, manifest, pluginRelative, diagnostics);
|
|
688
887
|
const files = tracked ?? listFiles(pluginRoot).map((file) => `${pluginRelative}/${file.relative}`);
|
|
@@ -694,7 +893,13 @@ function validatePluginRoot(pluginRoot, pluginName, tracked, diagnostics, schema
|
|
|
694
893
|
if (!isAllowed) extras.add(rel.split("/")[0]);
|
|
695
894
|
}
|
|
696
895
|
for (const extra of [...extras].sort()) {
|
|
697
|
-
diagnostics.push(
|
|
896
|
+
diagnostics.push(
|
|
897
|
+
diagnostic(
|
|
898
|
+
"plugin-root-entry",
|
|
899
|
+
`${pluginRelative}/${extra}`,
|
|
900
|
+
"Tracked plugin-root content must be manifest-declared, README.md, or an exact manifest asset reference.",
|
|
901
|
+
),
|
|
902
|
+
);
|
|
698
903
|
}
|
|
699
904
|
|
|
700
905
|
const skillResults = [];
|
|
@@ -703,11 +908,23 @@ function validatePluginRoot(pluginRoot, pluginName, tracked, diagnostics, schema
|
|
|
703
908
|
for (const entry of readdirSync(skillsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
704
909
|
if (isIgnoredNoise(entry.name)) continue;
|
|
705
910
|
if (entry.name.startsWith(".")) {
|
|
706
|
-
diagnostics.push(
|
|
911
|
+
diagnostics.push(
|
|
912
|
+
diagnostic(
|
|
913
|
+
"skills-hidden-entry",
|
|
914
|
+
`${pluginRelative}/skills/${entry.name}`,
|
|
915
|
+
"skills/ may not contain hidden entries.",
|
|
916
|
+
),
|
|
917
|
+
);
|
|
707
918
|
continue;
|
|
708
919
|
}
|
|
709
920
|
if (!entry.isDirectory()) {
|
|
710
|
-
diagnostics.push(
|
|
921
|
+
diagnostics.push(
|
|
922
|
+
diagnostic(
|
|
923
|
+
"skills-loose-file",
|
|
924
|
+
`${pluginRelative}/skills/${entry.name}`,
|
|
925
|
+
"skills/ may contain only skill directories.",
|
|
926
|
+
),
|
|
927
|
+
);
|
|
711
928
|
continue;
|
|
712
929
|
}
|
|
713
930
|
skillResults.push(validateSkill(join(skillsRoot, entry.name), pluginName));
|
|
@@ -747,11 +964,7 @@ function versionIncreased(before, after) {
|
|
|
747
964
|
export function validateVersionChanges({ repoRoot, currentRoot, base, staged = false }) {
|
|
748
965
|
const diagnostics = [];
|
|
749
966
|
const changed = changedFiles(repoRoot, base, staged);
|
|
750
|
-
const pluginNames = new Set(
|
|
751
|
-
changed
|
|
752
|
-
.map((path) => /^extensions\/([^/]+)\//.exec(path)?.[1])
|
|
753
|
-
.filter(Boolean),
|
|
754
|
-
);
|
|
967
|
+
const pluginNames = new Set(changed.map((path) => /^extensions\/([^/]+)\//.exec(path)?.[1]).filter(Boolean));
|
|
755
968
|
for (const pluginName of pluginNames) {
|
|
756
969
|
const prefix = `extensions/${pluginName}/`;
|
|
757
970
|
const manifestRel = `${prefix}.agents/plugin.json`;
|
|
@@ -760,13 +973,20 @@ export function validateVersionChanges({ repoRoot, currentRoot, base, staged = f
|
|
|
760
973
|
const currentManifestPath = join(currentRoot, manifestRel);
|
|
761
974
|
const currentManifest = isFile(currentManifestPath) ? readFileSync(currentManifestPath, "utf8") : null;
|
|
762
975
|
if (!previousManifest || !currentManifest) continue;
|
|
763
|
-
const nonVersionPluginChange =
|
|
976
|
+
const nonVersionPluginChange =
|
|
977
|
+
pluginChanges.some((path) => path !== manifestRel) ||
|
|
764
978
|
manifestWithoutVersion(previousManifest) !== manifestWithoutVersion(currentManifest);
|
|
765
979
|
if (!nonVersionPluginChange) continue;
|
|
766
980
|
const before = parseVersion(previousManifest);
|
|
767
981
|
const after = parseVersion(currentManifest);
|
|
768
982
|
if (!before || !after || !versionIncreased(before, after)) {
|
|
769
|
-
diagnostics.push(
|
|
983
|
+
diagnostics.push(
|
|
984
|
+
diagnostic(
|
|
985
|
+
"plugin-version-not-bumped",
|
|
986
|
+
manifestRel,
|
|
987
|
+
`Plugin content changed but version did not increase from ${before?.join(".") ?? "invalid"} to ${after?.join(".") ?? "invalid"}.`,
|
|
988
|
+
),
|
|
989
|
+
);
|
|
770
990
|
}
|
|
771
991
|
}
|
|
772
992
|
return diagnostics;
|
|
@@ -821,7 +1041,9 @@ export function validateRepository({ root, tracked = null }) {
|
|
|
821
1041
|
// nesting does not namespace it. Two extensions shipping one name there is
|
|
822
1042
|
// last-writer-wins, silently.
|
|
823
1043
|
const skillOwners = new Map();
|
|
824
|
-
for (const entry of readdirSync(extensionsRoot, { withFileTypes: true }).sort((a, b) =>
|
|
1044
|
+
for (const entry of readdirSync(extensionsRoot, { withFileTypes: true }).sort((a, b) =>
|
|
1045
|
+
a.name.localeCompare(b.name),
|
|
1046
|
+
)) {
|
|
825
1047
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
826
1048
|
const plugin = validatePluginRoot(join(extensionsRoot, entry.name), entry.name, presentTracked, diagnostics, root);
|
|
827
1049
|
for (const skill of plugin.skills) {
|
|
@@ -847,11 +1069,13 @@ export function validatePlugin({ pluginRoot, tracked = null }) {
|
|
|
847
1069
|
const root = findGitRoot(pluginRoot);
|
|
848
1070
|
const pluginName = basename(pluginRoot);
|
|
849
1071
|
const repoRelative = root ? toPosix(relative(root, pluginRoot)) : null;
|
|
850
|
-
const selectedTracked =
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1072
|
+
const selectedTracked =
|
|
1073
|
+
tracked ??
|
|
1074
|
+
(root && repoRelative?.startsWith("extensions/")
|
|
1075
|
+
? trackedFiles(root)
|
|
1076
|
+
.filter((path) => path.startsWith(`${repoRelative}/`))
|
|
1077
|
+
.filter((path) => isFile(join(root, path)))
|
|
1078
|
+
: null);
|
|
855
1079
|
const diagnostics = [];
|
|
856
1080
|
const result = validatePluginRoot(pluginRoot, pluginName, selectedTracked, diagnostics, VALIDATOR_ROOT);
|
|
857
1081
|
for (const skill of result.skills) diagnostics.push(...skill.diagnostics);
|