@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.
Files changed (74) hide show
  1. package/bundled-extensions.json +1 -4
  2. package/dist/create/claude.d.ts.map +1 -1
  3. package/dist/create/claude.js +1 -1
  4. package/dist/create/claude.js.map +1 -1
  5. package/dist/create/codex.d.ts.map +1 -1
  6. package/dist/create/codex.js +2 -3
  7. package/dist/create/codex.js.map +1 -1
  8. package/dist/create/cursor.d.ts.map +1 -1
  9. package/dist/create/cursor.js +3 -7
  10. package/dist/create/cursor.js.map +1 -1
  11. package/dist/create/enact.d.ts.map +1 -1
  12. package/dist/create/enact.js.map +1 -1
  13. package/dist/create/index.d.ts +3 -3
  14. package/dist/create/index.d.ts.map +1 -1
  15. package/dist/create/index.js +4 -4
  16. package/dist/create/index.js.map +1 -1
  17. package/dist/create/kimi.d.ts.map +1 -1
  18. package/dist/create/kimi.js +2 -3
  19. package/dist/create/kimi.js.map +1 -1
  20. package/dist/create/opencode.d.ts.map +1 -1
  21. package/dist/create/opencode.js +5 -12
  22. package/dist/create/opencode.js.map +1 -1
  23. package/dist/index.d.ts +12 -13
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -9
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal/agents.d.ts.map +1 -1
  28. package/dist/internal/agents.js +1 -1
  29. package/dist/internal/agents.js.map +1 -1
  30. package/dist/internal/extensions-table.d.ts +27 -6
  31. package/dist/internal/extensions-table.d.ts.map +1 -1
  32. package/dist/internal/extensions-table.js +85 -12
  33. package/dist/internal/extensions-table.js.map +1 -1
  34. package/dist/internal/file-ops.js +1 -1
  35. package/dist/internal/file-ops.js.map +1 -1
  36. package/dist/internal/io.d.ts.map +1 -1
  37. package/dist/internal/io.js.map +1 -1
  38. package/dist/internal/json-merge.d.ts.map +1 -1
  39. package/dist/internal/json-merge.js +4 -2
  40. package/dist/internal/json-merge.js.map +1 -1
  41. package/dist/internal/lock-ownership.d.ts.map +1 -1
  42. package/dist/internal/lock-ownership.js.map +1 -1
  43. package/dist/internal/pick.d.ts.map +1 -1
  44. package/dist/internal/pick.js.map +1 -1
  45. package/dist/internal/platform.d.ts.map +1 -1
  46. package/dist/internal/platform.js +7 -53
  47. package/dist/internal/platform.js.map +1 -1
  48. package/dist/internal/schema.d.ts.map +1 -1
  49. package/dist/internal/schema.js.map +1 -1
  50. package/dist/internal/toml-block.d.ts.map +1 -1
  51. package/dist/internal/toml-block.js.map +1 -1
  52. package/dist/projection.d.ts.map +1 -1
  53. package/dist/projection.js +2 -2
  54. package/dist/projection.js.map +1 -1
  55. package/dist/validate/index.d.ts +2 -2
  56. package/dist/validate/index.d.ts.map +1 -1
  57. package/dist/validate/index.js +3 -3
  58. package/dist/validate/index.js.map +1 -1
  59. package/dist/validate/run.d.ts.map +1 -1
  60. package/dist/validate/run.js +2 -5
  61. package/dist/validate/run.js.map +1 -1
  62. package/extensions/enact-repo/.agents/plugin.json +2 -2
  63. package/extensions/enact-repo/skills/interaction-design/references/animation-libraries-gsap.md +0 -1
  64. package/extensions/enact-repo/skills/interaction-design/references/animation-libraries.md +0 -1
  65. package/extensions/enact-repo/skills/interaction-design/references/scroll-animations.md +0 -1
  66. package/package.json +9 -8
  67. package/scripts/enact-extensions.mjs +331 -303
  68. package/scripts/lib/build-index.mjs +1 -3
  69. package/scripts/lib/resolve-bundle.mjs +1 -3
  70. package/scripts/lib/run-repo-doctor.mjs +50 -17
  71. package/scripts/lib/run-repo-install.mjs +130 -58
  72. package/scripts/lib/run-sync.mjs +3 -9
  73. package/scripts/lib/run-validate.mjs +1 -3
  74. 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
- "Factory artifacts go under `~/.enact/factory/projects/<key>/…`.";
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 = /`[^`]*(\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;
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
- ["slop-marketing", /\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],
62
- ["slop-filler", /\b(it'?s worth noting|it'?s important to note|note that|in summary|in conclusion|obviously|clearly)\b/i],
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(diagnostic("frontmatter-unterminated", path, "YAML frontmatter must end with a closing --- line."));
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(/\[[^\]]*]\((?:\.\/)?((?:scripts|references|assets)\/[A-Za-z0-9._/-]+)(?:#[^)\s]+)?\)/g)) {
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 = extension === ".py"
290
- ? ["python3", ["-c", "import ast,pathlib,sys; ast.parse(pathlib.Path(sys.argv[1]).read_text())", path]]
291
- : extension === ".sh"
292
- ? ["bash", ["-n", path]]
293
- : new Set([".js", ".mjs", ".cjs"]).has(extension)
294
- ? [process.execPath, ["--check", path]]
295
- : null;
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(diagnostic("script-extension", relativePath, `Unsupported script extension ${extension || "(none)"}.`));
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) => part.replace(/^#{1,6}\s+.*$/gm, "").replace(/[`*_>#-]/g, " ").replace(/\s+/g, " ").trim().toLowerCase())
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(diagnostic("slop-duplicate-paragraph", path, `Duplicate prose paragraph also appears in: ${sameBundleOthers.join(", ")}.`));
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)) diagnostics.push(diagnostic("slop-emoji", path, "Emoji and decorative symbols are not allowed."));
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(diagnostic("skill-route-missing", skill.path, `Referenced skill does not exist in this plugin: ${target}.`));
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(diagnostic("manifest-schema", `${pluginRelative}/.agents/plugin.json`, "Canonical schema spec/enact.json is missing."));
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(diagnostic(
387
- "manifest-schema",
388
- `${pluginRelative}/.agents/plugin.json`,
389
- `${error.instancePath || "/"}: ${error.message ?? "invalid"}.`,
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(diagnostic("manifest-schema", `${pluginRelative}/.agents/plugin.json`, `Cannot validate canonical manifest: ${err instanceof Error ? err.message : String(err)}`));
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(diagnostic("skill-root-file", relativePath, "Only SKILL.md, LICENSE, LICENSE.*, and version.json may be files directly under a skill root."));
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(diagnostic("skill-root-directory", relativePath, "Only scripts/, references/, and assets/ are allowed under a skill root."));
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) diagnostics.push(diagnostic("skill-symlink", fullRelative, "Symlinks are not allowed in a skill."));
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(diagnostic("skill-missing", `${rootRelative}/SKILL.md`, "Every skill directory requires SKILL.md."));
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(diagnostic("skill-line-limit", skillRelative, `SKILL.md has ${lineCount(text)} lines; maximum is 100.`));
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(diagnostic("frontmatter-key", skillRelative, `Unexpected frontmatter key ${JSON.stringify(key)}.`));
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(diagnostic("frontmatter-name", skillRelative, `name must equal directory name ${JSON.stringify(skillName)}.`));
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(diagnostic("frontmatter-description-slop", skillRelative, "Use a direct description; do not start with generic trigger boilerplate."));
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(diagnostic("frontmatter-invocation", skillRelative, "disable-model-invocation must be an explicit boolean."));
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(diagnostic("frontmatter-compatibility", skillRelative, "compatibility must be a string when present."));
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(diagnostic(
482
- "heading-contract",
483
- skillRelative,
484
- `SKILL.md must contain H2 headings \`${useWhen}\` and \`${doNotUseWhen}\`, with \`${useWhen}\` before \`${doNotUseWhen}\`. Any other H2 headings are allowed, in any order.`,
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(diagnostic("boundaries-contract", skillRelative, `## Boundaries must contain exact H3 headings: ${REQUIRED_BOUNDARY_HEADINGS.join(" → ")}.`));
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(diagnostic("completion-proof", skillRelative, "## Completion requires a command-shaped proof line."));
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(diagnostic("storage-duplicate", skillRelative, "The canonical Factory storage sentence may appear at most once."));
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 = h1Line === undefined
514
- ? null
515
- : body.split(/\r?\n/).slice(h1Line).find((line) => line.trim())?.trim() ?? null;
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(diagnostic("storage-contract", skillRelative, `Factory path users must place exactly this sentence immediately after the H1: ${CANONICAL_FACTORY_STORAGE}`));
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(/~\/\.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);
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(diagnostic("storage-noncanonical", skillRelative, "Use only the canonical Factory storage sentence."));
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(diagnostic("resource-missing", current.fullRelative, `Local resource reference does not exist: ${reference}.`));
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(diagnostic("resource-line-limit", resource.fullRelative, `Markdown resource has ${lineCount(resourceText)} lines; maximum is 300.`));
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(diagnostic("resource-orphan", resource.fullRelative, "Resource is not reachable from SKILL.md."));
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(diagnostic("manifest-path", `${pluginRelative}/.agents/plugin.json`, `${field} must be a safe plugin-relative path.`));
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(diagnostic("manifest-path-missing", `${pluginRelative}/.agents/plugin.json`, `${field} path does not exist: ${raw}`));
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(diagnostic("manifest-asset", `${pluginRelative}/.agents/plugin.json`, `${field} must reference an existing plugin-relative file.`));
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(diagnostic("assets-path", `${pluginRelative}/.agents/plugin.json`, "assets must be an array of plugin-relative paths."));
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(diagnostic("assets-path", `${pluginRelative}/.agents/plugin.json`, `assets entry must be a safe, existing plugin-relative path: ${JSON.stringify(raw)}`));
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(diagnostic("principles-config", `${pluginRelative}/.agents/plugin.json`, "principles must reference an existing plugin-relative config file."));
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(diagnostic("principles-config", `${pluginRelative}/${resolved.relative}`, "Prompt config must declare a fragments array."));
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(diagnostic("principles-fragment", `${pluginRelative}/${resolved.relative}`, "Each prompt fragment must reference an existing file inside the plugin."));
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(diagnostic("principles-config", `${pluginRelative}/${resolved.relative}`, `Cannot parse prompt config: ${err instanceof Error ? err.message : String(err)}`));
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(manifest.validate);
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(diagnostic("manifest-validate-script", `${pluginRelative}/.agents/plugin.json`, `validate references a missing local script: ${localScript[1]}`));
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(diagnostic("manifest-missing", `${pluginRelative}/.agents/plugin.json`, "Plugin requires a canonical .agents/plugin.json."));
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(diagnostic("manifest-invalid-json", `${pluginRelative}/.agents/plugin.json`, err instanceof Error ? err.message : String(err)));
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(diagnostic("manifest-shape", `${pluginRelative}/.agents/plugin.json`, "Plugin manifest must be an object."));
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 = Array.isArray(manifest.targets) && manifest.targets.at(-1) === OPT_IN_TARGET
679
- ? manifest.targets.slice(0, -1)
680
- : manifest.targets;
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(diagnostic("manifest-targets", `${pluginRelative}/.agents/plugin.json`, `targets must be exactly ${JSON.stringify(ALL_SURFACE_TARGETS)}, optionally followed by "${OPT_IN_TARGET}".`));
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(diagnostic("manifest-version", `${pluginRelative}/.agents/plugin.json`, "version must be semantic MAJOR.MINOR.PATCH."));
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(diagnostic("plugin-root-entry", `${pluginRelative}/${extra}`, "Tracked plugin-root content must be manifest-declared, README.md, or an exact manifest asset reference."));
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(diagnostic("skills-hidden-entry", `${pluginRelative}/skills/${entry.name}`, "skills/ may not contain hidden entries."));
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(diagnostic("skills-loose-file", `${pluginRelative}/skills/${entry.name}`, "skills/ may contain only skill directories."));
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 = pluginChanges.some((path) => path !== manifestRel) ||
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(diagnostic("plugin-version-not-bumped", manifestRel, `Plugin content changed but version did not increase from ${before?.join(".") ?? "invalid"} to ${after?.join(".") ?? "invalid"}.`));
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) => a.name.localeCompare(b.name))) {
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 = tracked ?? (root && repoRelative?.startsWith("extensions/")
851
- ? trackedFiles(root)
852
- .filter((path) => path.startsWith(`${repoRelative}/`))
853
- .filter((path) => isFile(join(root, path)))
854
- : null);
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);