universal-plugin 0.8.0 → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
package/dist/cli.mjs CHANGED
@@ -5247,6 +5247,274 @@ function discardedRangeWarning(index, entry, range) {
5247
5247
  return `dependencies[${index}] "${entry}" carries a version range the runtime discards — declare ${marketplace ? `{ "name": "${name}", "marketplace": "${marketplace}", "version": "${range}" }` : `{ "name": "${name}", "version": "${range}" }`} for a range that is enforced`;
5248
5248
  }
5249
5249
  //#endregion
5250
+ //#region src/governance/copy.ts
5251
+ /** The governance copy step: a governance is copied into every skill that uses it at build time and
5252
+ * read from disk at run time, so a skill never pays a registry lookup to read a rule set it was
5253
+ * tested against (repobuddy/buddy-agent-harness#122).
5254
+ *
5255
+ * The files already present in `<skill>/references/governances/` ARE the declaration of which
5256
+ * governances that skill uses — nothing is declared in plugin.json. The build refreshes every file
5257
+ * in that folder from its owning package, adds the governances those files reference, and fails
5258
+ * when a file names no governance or when SKILL.md does not list a copy. */
5259
+ /** Where a skill's copies live, relative to the skill folder. Posix spelling: it is both a path
5260
+ * segment and the text a SKILL.md References section has to carry. */
5261
+ const SKILL_GOVERNANCE_DIR = "references/governances";
5262
+ /** A retrieval pointer inside a governance body: `<runner> <package> governance show <name>`, or the
5263
+ * bare `governance show <name>` in prose. Only the trailing two words carry meaning here — which
5264
+ * package shipped the command is exactly what the copy step removes. */
5265
+ const POINTER = /governance show\s+([A-Za-z0-9][A-Za-z0-9._/-]*)/g;
5266
+ /** A fenced code block opener or closer, with any indentation and info string. */
5267
+ const FENCE = /^\s*(?:```|~~~)/;
5268
+ /** Indentation, plus a list marker when the pointer is a bullet. A pointer listed under References
5269
+ * is usually one bullet among several, and an instruction that loses the bullet leaves the list. */
5270
+ const LINE_PREFIX = /^\s*(?:[-*+]\s+|\d+\.\s+)?/;
5271
+ /** The copy's name for a pointer token: a namespaced `<plugin>/<name>` lookup and a plain `<name>`
5272
+ * resolve to the same document, so both land in the same file. */
5273
+ function governanceName(token) {
5274
+ const slash = token.lastIndexOf("/");
5275
+ const name = slash === -1 ? token : token.slice(slash + 1);
5276
+ return name.endsWith(".md") ? name.slice(0, -3) : name;
5277
+ }
5278
+ /** The governances a governance body points at, in first-appearance order, deduplicated. */
5279
+ function referencedGovernances(content) {
5280
+ const names = [];
5281
+ for (const match of content.matchAll(POINTER)) {
5282
+ const token = match[1];
5283
+ if (!token) continue;
5284
+ const name = governanceName(token);
5285
+ if (!names.includes(name)) names.push(name);
5286
+ }
5287
+ return names;
5288
+ }
5289
+ /** The instruction a pointer becomes. Stated once so the rewriter and its tests cannot drift. */
5290
+ function loadInstruction(name) {
5291
+ return `Load \`${SKILL_GOVERNANCE_DIR}/${name}.md\` if it is not already loaded.`;
5292
+ }
5293
+ /** Rewrites every retrieval pointer in a copy into an instruction to load the sibling copy. The
5294
+ * Agent Skills specification asks that every file an agent needs is one level deep from SKILL.md,
5295
+ * so a copy must never send the agent back to a command to reach the next document.
5296
+ *
5297
+ * A fenced block whose every line was a pointer is unwrapped: prose in a bash fence reads as a
5298
+ * command to run. A block that mixes pointers with real commands keeps its fence. */
5299
+ function rewriteGovernancePointers(content) {
5300
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
5301
+ const entries = content.split(/\r?\n/).map((line) => {
5302
+ if (FENCE.test(line)) return {
5303
+ lines: [line],
5304
+ rewritten: false,
5305
+ fence: true
5306
+ };
5307
+ const names = referencedGovernances(line);
5308
+ if (names.length === 0) return {
5309
+ lines: [line],
5310
+ rewritten: false,
5311
+ fence: false
5312
+ };
5313
+ const prefix = LINE_PREFIX.exec(line)?.[0] ?? "";
5314
+ return {
5315
+ lines: names.map((name) => `${prefix}${loadInstruction(name)}`),
5316
+ rewritten: true,
5317
+ fence: false
5318
+ };
5319
+ });
5320
+ const out = [];
5321
+ for (let i = 0; i < entries.length; i++) {
5322
+ const entry = entries[i];
5323
+ if (!entry.fence) {
5324
+ out.push(...entry.lines);
5325
+ continue;
5326
+ }
5327
+ const close = entries.findIndex((e, j) => j > i && e.fence);
5328
+ const inner = close === -1 ? [] : entries.slice(i + 1, close);
5329
+ if (!(close !== -1 && inner.some((e) => e.rewritten) && inner.every((e) => e.rewritten || e.lines.every((l) => l.trim() === "")))) {
5330
+ out.push(...entry.lines);
5331
+ continue;
5332
+ }
5333
+ for (const e of inner) out.push(...e.lines);
5334
+ i = close;
5335
+ }
5336
+ return out.join(eol);
5337
+ }
5338
+ /** The copies a SKILL.md fails to list under References. The specification asks SKILL.md to
5339
+ * reference every file directly, so a copy no References section names is a file the agent has no
5340
+ * documented way to reach. */
5341
+ function unlistedGovernanceCopies(skillMd, names) {
5342
+ const section = referencesSection(skillMd);
5343
+ return names.filter((name) => !section.includes(`${SKILL_GOVERNANCE_DIR}/${name}.md`));
5344
+ }
5345
+ /** The body of the References section, or the empty string when the document has none. */
5346
+ function referencesSection(skillMd) {
5347
+ const lines = skillMd.split(/\r?\n/);
5348
+ const start = lines.findIndex((line) => /^(#{1,6})\s+references\b/i.test(line));
5349
+ if (start === -1) return "";
5350
+ const level = (/^(#{1,6})/.exec(lines[start])?.[1] ?? "#").length;
5351
+ let end = lines.length;
5352
+ for (let i = start + 1; i < lines.length; i++) {
5353
+ const heading = /^(#{1,6})\s/.exec(lines[i]);
5354
+ if (heading && heading[1].length <= level) {
5355
+ end = i;
5356
+ break;
5357
+ }
5358
+ }
5359
+ return lines.slice(start + 1, end).join("\n");
5360
+ }
5361
+ //#endregion
5362
+ //#region src/governance/copy-sync.ts
5363
+ /** Where a package keeps the governances it owns. A governance ships in its owner's package as a
5364
+ * plain Markdown file — the copy step needs nothing else from the owner. */
5365
+ const OWNER_DIR = "governances";
5366
+ /** Refreshes every skill's governance copies from their owning packages.
5367
+ *
5368
+ * The files already in `<skill>/references/governances/` declare which governances the skill uses.
5369
+ * Each is rewritten from its owner's current copy, every governance a copy references is copied
5370
+ * too, and the retrieval pointers inside each copy are rewritten to name the sibling file. */
5371
+ function syncGovernanceCopies(root, skills, govFs, opts = {}) {
5372
+ const entries = [];
5373
+ const errors = [];
5374
+ const owners = ownerDirs(root, govFs);
5375
+ for (const skill of skills) {
5376
+ const skillDir = path.dirname(skill.path);
5377
+ const copyDir = path.join(skillDir, ...SKILL_GOVERNANCE_DIR.split("/"));
5378
+ if (!govFs.isDirectory(copyDir)) continue;
5379
+ const copied = [];
5380
+ const declared = govFs.list(copyDir);
5381
+ const queue = declared.map((name) => ({ name }));
5382
+ const seen = new Set(declared);
5383
+ while (queue.length > 0) {
5384
+ const wanted = queue.shift();
5385
+ const { name } = wanted;
5386
+ const source = resolveSource(name, owners, govFs);
5387
+ if (!source) {
5388
+ errors.push(unresolvedMessage(skill.name, wanted, owners));
5389
+ continue;
5390
+ }
5391
+ copied.push(name);
5392
+ for (const referenced of referencedGovernances(source.content)) {
5393
+ if (seen.has(referenced)) continue;
5394
+ seen.add(referenced);
5395
+ queue.push({
5396
+ name: referenced,
5397
+ via: name
5398
+ });
5399
+ }
5400
+ const content = rewriteGovernancePointers(source.content);
5401
+ const target = path.join(copyDir, `${name}.md`);
5402
+ if ((govFs.exists(target) ? govFs.read(target) : null) === content) {
5403
+ entries.push({
5404
+ skill: skill.name,
5405
+ name,
5406
+ path: target,
5407
+ owner: source.owner,
5408
+ status: "unchanged"
5409
+ });
5410
+ continue;
5411
+ }
5412
+ if (opts.check || opts.dryRun) {
5413
+ entries.push({
5414
+ skill: skill.name,
5415
+ name,
5416
+ path: target,
5417
+ owner: source.owner,
5418
+ status: "stale"
5419
+ });
5420
+ continue;
5421
+ }
5422
+ govFs.write(target, content);
5423
+ entries.push({
5424
+ skill: skill.name,
5425
+ name,
5426
+ path: target,
5427
+ owner: source.owner,
5428
+ status: "written"
5429
+ });
5430
+ }
5431
+ const unlisted = unlistedGovernanceCopies(govFs.read(skill.path), copied);
5432
+ for (const name of unlisted) errors.push(`skill "${skill.name}": SKILL.md does not list \`${SKILL_GOVERNANCE_DIR}/${name}.md\` under References.`);
5433
+ }
5434
+ return {
5435
+ entries,
5436
+ errors
5437
+ };
5438
+ }
5439
+ function unresolvedMessage(skill, wanted, owners) {
5440
+ const checked = `No installed package owns it — checked ${owners.map((o) => o.name).join(", ")}.`;
5441
+ return wanted.via ? `skill "${skill}": governance "${wanted.via}" references "${wanted.name}", which has no copy to point at. ${checked}` : `skill "${skill}": ${SKILL_GOVERNANCE_DIR}/${wanted.name}.md names no governance. ${checked}`;
5442
+ }
5443
+ function resolveSource(name, owners, govFs) {
5444
+ for (const owner of owners) {
5445
+ const filePath = path.join(owner.dir, `${name}.md`);
5446
+ if (govFs.exists(filePath)) return {
5447
+ owner: owner.name,
5448
+ content: govFs.read(filePath)
5449
+ };
5450
+ }
5451
+ return null;
5452
+ }
5453
+ /** The packages a governance may come from, in resolution order: the plugin being built, then its
5454
+ * declared dependencies. The plugin wins — a plugin that ships a governance owns that name, and a
5455
+ * build must not copy a stale published copy of a document its own tree holds. */
5456
+ function ownerDirs(root, govFs) {
5457
+ const owners = [{
5458
+ name: ".",
5459
+ dir: path.join(root, OWNER_DIR)
5460
+ }];
5461
+ for (const pkg of declaredDependencies(root, govFs)) {
5462
+ const dir = packageDir(root, pkg, govFs);
5463
+ if (dir) owners.push({
5464
+ name: pkg,
5465
+ dir: path.join(dir, OWNER_DIR)
5466
+ });
5467
+ }
5468
+ return owners;
5469
+ }
5470
+ /** Every package the plugin's own `package.json` declares. A governance owner is installed as a dev
5471
+ * dependency of the repository being built; runtime dependencies are read too, because which of
5472
+ * the two a plugin author picked is not this step's business. */
5473
+ function declaredDependencies(root, govFs) {
5474
+ const manifestPath = path.join(root, "package.json");
5475
+ if (!govFs.exists(manifestPath)) return [];
5476
+ let parsed;
5477
+ try {
5478
+ parsed = JSON.parse(govFs.read(manifestPath));
5479
+ } catch {
5480
+ return [];
5481
+ }
5482
+ const names = /* @__PURE__ */ new Set();
5483
+ for (const field of [parsed.devDependencies, parsed.dependencies]) if (field && typeof field === "object") for (const name of Object.keys(field)) names.add(name);
5484
+ return [...names];
5485
+ }
5486
+ /** Walks up from the plugin root looking for the installed package, the way Node resolves one. A
5487
+ * workspace hoists its dependencies to the repository root, so the first `node_modules` above the
5488
+ * plugin is usually not the one holding them. */
5489
+ function packageDir(root, pkg, govFs) {
5490
+ let dir = path.resolve(root);
5491
+ for (;;) {
5492
+ const candidate = path.join(dir, "node_modules", pkg);
5493
+ if (govFs.exists(path.join(candidate, "package.json"))) return candidate;
5494
+ const parent = path.dirname(dir);
5495
+ if (parent === dir) return null;
5496
+ dir = parent;
5497
+ }
5498
+ }
5499
+ //#endregion
5500
+ //#region src/governance/fs.ts
5501
+ const realGovernanceFs = {
5502
+ exists: (p) => fs.existsSync(p),
5503
+ read: (p) => fs.readFileSync(p, "utf8"),
5504
+ list: (dir) => {
5505
+ if (!fs.existsSync(dir)) return [];
5506
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
5507
+ }
5508
+ };
5509
+ const realGovernanceCopyFs = {
5510
+ ...realGovernanceFs,
5511
+ isDirectory: (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory(),
5512
+ write: (filePath, content) => {
5513
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5514
+ fs.writeFileSync(filePath, content);
5515
+ }
5516
+ };
5517
+ //#endregion
5250
5518
  //#region src/hooks/hooks.ts
5251
5519
  /** Per-vendor hook support. Source: `.research/hook-event-survey/conclusion.md`, re-verified against
5252
5520
  * vendor documentation August 2026. A vendor fact decays — re-verify before trusting this table. */
@@ -6215,6 +6483,10 @@ function validateManifest(manifest, targets) {
6215
6483
  return errors;
6216
6484
  }
6217
6485
  function buildPlugin(root, opts = {}) {
6486
+ if (opts.check) opts = {
6487
+ ...opts,
6488
+ dryRun: true
6489
+ };
6218
6490
  const manifestPath = path.join(root, "plugin.json");
6219
6491
  if (!fs.existsSync(manifestPath)) throw new Error(`No plugin.json found at ${root}`);
6220
6492
  const indent = detectIndent(fs.readFileSync(manifestPath, "utf8"));
@@ -6241,7 +6513,7 @@ function buildPlugin(root, opts = {}) {
6241
6513
  }
6242
6514
  if (vendors.length === 0) {
6243
6515
  const signals = legacyLayoutSignals(manifest, fs.existsSync(path.join(root, SHADOWING_MANIFEST)));
6244
- if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor for the full diagnosis and the skill that owns each repair.`);
6516
+ if (signals.length > 0) throw new Error(`Nothing was derived, and this project is still on the pre-0.6 manifest layout:\n${signals.map((s) => ` - ${s}`).join("\n")}\nRun /universal-plugin:doctor-universal-plugin for the full diagnosis and the skill that owns each repair.`);
6245
6517
  warnings.push("No vendors declared in harnesses — nothing to build");
6246
6518
  return {
6247
6519
  vendors: [],
@@ -6249,6 +6521,7 @@ function buildPlugin(root, opts = {}) {
6249
6521
  warnings,
6250
6522
  rows,
6251
6523
  catalogs: [],
6524
+ governances: [],
6252
6525
  summary: summarize(rows)
6253
6526
  };
6254
6527
  }
@@ -6259,6 +6532,12 @@ function buildPlugin(root, opts = {}) {
6259
6532
  const { vendors: _vendors, packagePath: _packagePath, harnesses: _harnesses, dependencies: declaredDependencies, ...componentConfig } = uext;
6260
6533
  warnings.push(...validateDependencies(declaredDependencies).warnings);
6261
6534
  const skills = readSkills(root, manifest, warnings);
6535
+ const governances = syncGovernanceCopies(root, skills, realGovernanceCopyFs, {
6536
+ check: opts.check,
6537
+ dryRun: opts.dryRun
6538
+ });
6539
+ if (governances.errors.length > 0) throw new Error(`Governance copies are not buildable:\n${governances.errors.map((e) => ` - ${e}`).join("\n")}`);
6540
+ written.push(...governances.entries.filter((e) => e.status === "written").map((e) => e.path));
6262
6541
  const canonicalHooks = readCanonicalHooks(root, componentConfig["hooks"], warnings);
6263
6542
  const declaredMcp = readCanonicalMcpServers(root, componentConfig["mcpServers"], warnings);
6264
6543
  const mcp = declaredMcp ? pinMcpServers(declaredMcp.servers, manifest.version) : null;
@@ -6341,6 +6620,7 @@ function buildPlugin(root, opts = {}) {
6341
6620
  rows,
6342
6621
  catalogs,
6343
6622
  catalogRoot,
6623
+ governances: governances.entries,
6344
6624
  summary: summarize(rows)
6345
6625
  };
6346
6626
  }
@@ -6681,8 +6961,8 @@ function formatMcpNote(note) {
6681
6961
  }
6682
6962
  //#endregion
6683
6963
  //#region src/build/cli.ts
6684
- const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor — diagnose why nothing is declared\n";
6685
- const NEXT_STEP_BUILT = "→ /universal-plugin:doctor — check the built manifests against plugin.json\n";
6964
+ const NEXT_STEP_NOTHING_BUILT = "→ /universal-plugin:doctor-universal-plugin — diagnose why nothing is declared\n";
6965
+ const NEXT_STEP_BUILT = "→ /universal-plugin:doctor-universal-plugin — check the built manifests against plugin.json\n";
6686
6966
  /** Every hint names a command that exists today: an agent that follows a hint into
6687
6967
  * `unknown command` is at a dead end (issue #80). A refreshed catalog is checked by
6688
6968
  * `marketplace validate`, pointed at the repository the catalog lives in. */
@@ -6693,19 +6973,27 @@ function nextStep$1(result, cwd) {
6693
6973
  const quoted = /\s/.test(relative) ? JSON.stringify(relative) : relative;
6694
6974
  return `→ universal-plugin marketplace validate${relative === "" ? "" : ` --root ${quoted}`}\n`;
6695
6975
  }
6976
+ /** What repairs a stale copy: the same build, without `--check`. */
6977
+ function checkNextStep(root) {
6978
+ return `\u2192 universal-plugin plugin build${root ? ` --root ${root}` : ""} \u2014 refresh the stale governance copies\n`;
6979
+ }
6696
6980
  function buildCommand() {
6697
6981
  const cmd = new Command("build").description("Generate vendor manifests from plugin.json");
6698
- cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action((opts) => {
6982
+ cmd.option("--vendor <id>", "Build only the named vendor").option("--dry-run", "Print what would be written without writing").option("--verbose", "Print field-by-field transformation decisions").option("--clean", "Delete generated manifests before building").option("--check", "Write nothing; fail when a committed governance copy differs from its source").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin build --vendor claude-code\n").action((opts) => {
6699
6983
  try {
6700
6984
  const result = buildPlugin(resolveRoot(opts.root), {
6701
6985
  vendor: opts.vendor,
6702
6986
  dryRun: opts.dryRun,
6703
6987
  verbose: opts.verbose,
6704
- clean: opts.clean
6988
+ clean: opts.clean,
6989
+ check: opts.check
6705
6990
  });
6706
6991
  for (const warning of result.warnings) process.stderr.write(`warn: ${warning}\n`);
6992
+ const stale = result.governances.filter((g) => g.status === "stale");
6993
+ if (opts.check) for (const copy of stale) process.stderr.write(`stale: ${path.relative(process.cwd(), copy.path)} (owner ${copy.owner})\n`);
6707
6994
  const { built, skipped, failed, canonical } = result.summary;
6708
6995
  const jsonResult = {
6996
+ governances: result.governances,
6709
6997
  built: result.rows.filter((r) => r.status === "built"),
6710
6998
  skipped: result.rows.filter((r) => r.status === "skipped"),
6711
6999
  failed: result.rows.filter((r) => r.status === "failed"),
@@ -6716,6 +7004,7 @@ function buildCommand() {
6716
7004
  };
6717
7005
  const counts = `built ${built}, skipped ${skipped}, failed ${failed}`;
6718
7006
  const catalogSummary = result.catalogs.length > 0 ? `, catalogs ${result.catalogs.length}` : "";
7007
+ const governanceSummary = result.governances.length > 0 ? `, governance copies ${result.governances.length}` : "";
6719
7008
  output(jsonResult, {
6720
7009
  vendors: result.rows.map((r) => ({
6721
7010
  vendor: r.vendor,
@@ -6726,10 +7015,16 @@ function buildCommand() {
6726
7015
  path: r.path,
6727
7016
  status: r.status
6728
7017
  })),
6729
- summary: (canonical > 0 ? `${counts}, served by plugin.json ${canonical}` : counts) + catalogSummary
7018
+ ...result.governances.length > 0 ? { governances: result.governances.map((g) => ({
7019
+ skill: g.skill,
7020
+ name: g.name,
7021
+ status: g.status
7022
+ })) } : {},
7023
+ summary: (canonical > 0 ? `${counts}, served by plugin.json ${canonical}` : counts) + catalogSummary + governanceSummary
6730
7024
  });
6731
- process.stderr.write(nextStep$1(result, process.cwd()));
7025
+ process.stderr.write(opts.check && stale.length > 0 ? checkNextStep(opts.root) : nextStep$1(result, process.cwd()));
6732
7026
  if (failed > 0) process.exitCode = 1;
7027
+ if (opts.check && stale.length > 0) process.exitCode = 1;
6733
7028
  } catch (err) {
6734
7029
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
6735
7030
  process.exit(1);
@@ -7156,16 +7451,6 @@ function configCommand(fs = realConfigFs) {
7156
7451
  return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand(fs)).addCommand(getCommand(fs));
7157
7452
  }
7158
7453
  //#endregion
7159
- //#region src/governance/fs.ts
7160
- const realGovernanceFs = {
7161
- exists: (p) => fs.existsSync(p),
7162
- read: (p) => fs.readFileSync(p, "utf8"),
7163
- list: (dir) => {
7164
- if (!fs.existsSync(dir)) return [];
7165
- return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
7166
- }
7167
- };
7168
- //#endregion
7169
7454
  //#region src/governance/governance.ts
7170
7455
  function getManagedDir() {
7171
7456
  if (process.platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
@@ -63,12 +63,13 @@
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsdown",
66
+ "check:governances": "tsx src/cli.ts plugin build --check --root .",
66
67
  "dev": "tsx src/cli.ts",
67
68
  "knip": "knip",
68
69
  "lint": "biome check .",
69
70
  "test": "pnpm build && vitest run src",
70
71
  "test:watch": "vitest",
71
72
  "typecheck": "tsc --noEmit",
72
- "verify": "pnpm typecheck && pnpm lint && pnpm test"
73
+ "verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm check:governances"
73
74
  }
74
75
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "universal-plugin",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
6
6
  "author": {
7
7
  "name": "unional"
@@ -1,4 +1,4 @@
1
- # doctor skill
1
+ # doctor-universal-plugin skill
2
2
 
3
3
  Diagnose a universal agent plugin: what the canonical `plugin.json` declares, and whether what is on
4
4
  disk still matches it for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: doctor
2
+ name: doctor-universal-plugin
3
3
  description: Use this skill to diagnose a universal agent plugin — when `plugin build` reports "built 0" or "nothing to build", when it warns "No vendors declared in harnesses", when a repository still carries `.plugin/plugin.json` or a top-level `vendorExtensions` block after upgrading universal-plugin across a major, when a released version never reached the vendor manifests, when a runtime loads none of the plugin's skills, when a vendor manifest is missing or looks out of date after a pull, or when checking whether what the canonical plugin.json declares still matches what is on disk for Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "is my plugin set up right", "why isn't my plugin loading", "the build says built 0", "why did nothing get built", "check the plugin", "are the vendor manifests current", or "what does this plugin declare".
4
4
  ---
5
5