@vegastack/skills 0.1.0 → 0.2.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.
Files changed (66) hide show
  1. package/README.md +27 -7
  2. package/dist/index.js +191 -95
  3. package/package.json +16 -5
  4. package/skill/{vegastack-arch-guardian → arch-guardian}/SKILL.md +1 -1
  5. package/skill/arch-guardian/agents/openai.yaml +4 -0
  6. package/skill/{vegastack-arch-guardian → arch-guardian}/assets/architecture-profile.schema.json +1 -1
  7. package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/REFRESH.md +1 -1
  8. package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/sources.json +10 -10
  9. package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/refresh-evidence.mjs +4 -1
  10. package/skill/skill-maintainer/SKILL.md +79 -0
  11. package/skill/skill-maintainer/agents/openai.yaml +4 -0
  12. package/skill/skill-maintainer/references/release-ops.md +63 -0
  13. package/skill/skill-maintainer/references/standards.md +92 -0
  14. package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
  15. package/skill/skill-maintainer/refresh/sources.json +103 -0
  16. package/skill/skillify/SKILL.md +154 -0
  17. package/skill/skillify/agents/openai.yaml +4 -0
  18. package/skill/skillify/assets/templates/README.md.template +26 -0
  19. package/skill/skillify/assets/templates/REFRESH.md.template +21 -0
  20. package/skill/skillify/assets/templates/SKILL.md.template +26 -0
  21. package/skill/skillify/assets/templates/openai.yaml.template +4 -0
  22. package/skill/skillify/assets/templates/skill.test.ts.template +40 -0
  23. package/skill/skillify/assets/templates/sources.json.template +6 -0
  24. package/skill/skillify/references/authoring.md +72 -0
  25. package/skill/skillify/references/eval-playbook.md +62 -0
  26. package/skill/skillify/refresh/REFRESH.md +24 -0
  27. package/skill/skillify/refresh/sources.json +6 -0
  28. package/skill/skillify/scripts/scaffold-skill.mjs +138 -0
  29. package/skill-integrity.json +75 -45
  30. package/skill/vegastack-arch-guardian/agents/openai.yaml +0 -4
  31. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/adr-template.md +0 -0
  32. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/answers-example.json +0 -0
  33. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/architecture-profile.json +0 -0
  34. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/deployment-review-template.md +0 -0
  35. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/service-design-template.md +0 -0
  36. /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/threat-model-template.md +0 -0
  37. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/agent-product.md +0 -0
  38. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-cost.md +0 -0
  39. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-data-boundaries.md +0 -0
  40. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-evals.md +0 -0
  41. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/connectors-sandbox.md +0 -0
  42. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/data-memory.md +0 -0
  43. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/delivery-operations.md +0 -0
  44. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/durable-execution.md +0 -0
  45. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/flutter.md +0 -0
  46. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/foundation.md +0 -0
  47. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/hosting-reliability.md +0 -0
  48. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/identity-tenancy.md +0 -0
  49. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/model-lifecycle.md +0 -0
  50. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/models-observability.md +0 -0
  51. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/realtime-channels.md +0 -0
  52. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/security-privacy.md +0 -0
  53. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/topology-monorepo.md +0 -0
  54. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/web.md +0 -0
  55. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/control-catalog.json +0 -0
  56. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/foundation-compatibility.json +0 -0
  57. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/golden-patterns.md +0 -0
  58. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/profile-governance.md +0 -0
  59. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/rule-model.json +0 -0
  60. /package/skill/{vegastack-arch-guardian → arch-guardian}/references/workflows.md +0 -0
  61. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/architecture-check.mjs +0 -0
  62. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/lib.mjs +0 -0
  63. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/profile-tool.mjs +0 -0
  64. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/schema-validate.mjs +0 -0
  65. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/validate-profile.mjs +0 -0
  66. /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/verify-corpus.mjs +0 -0
package/README.md CHANGED
@@ -1,31 +1,51 @@
1
1
  # @vegastack/skills
2
2
 
3
- Installer for VegaStack Agent Skills. Installs the **vegastack-arch-guardian** skill an architecture advisor with stable normative rules and deterministic checks into Claude Code (`.claude/skills`) and Codex (`.agents/skills`).
3
+ Installer for VegaStack Agent Skills a family of self-contained skills for Claude Code, Codex, and Hermes, shipped in one integrity-checked package.
4
4
 
5
5
  ```sh
6
- npx @vegastack/skills add vegastack-arch-guardian
6
+ npx @vegastack/skills list
7
+ npx @vegastack/skills add arch-guardian
7
8
  ```
8
9
 
10
+ ## Skills in this package
11
+
12
+ | Skill | What it does |
13
+ |---|---|
14
+ | `arch-guardian` | Architecture advisor: capability-scoped normative rules, deterministic checks against a committed profile, exception/ADR governance |
15
+ | `skill-maintainer` | Encodes the Agent Skills standards (Claude Code, Codex, Hermes, agentskills.io) for creating, updating, and releasing skills in a skills repo |
16
+ | `skillify` | Turns a feature or workflow into a complete skill conforming to the VegaStack skills contract, or audits an existing one |
17
+
9
18
  ## Commands
10
19
 
11
20
  | Command | What it does |
12
21
  |---|---|
13
- | `add <skill>` | Install (or upgrade) the skill into the selected agent directories |
14
- | `verify <skill>` | Check an installed copy against the bundled checksum manifest |
15
- | `doctor` | Diagnose an install: integrity, profile presence, installed-vs-latest version |
16
- | `remove <skill>` | Uninstall the skill from the selected agent directories |
22
+ | `list` | Show the bundled skills |
23
+ | `add <skill>` | Install (or upgrade) a skill into the selected agent directories |
24
+ | `verify [skill]` | Check installed copies against the bundled checksum manifest (all bundled skills when no name given) |
25
+ | `doctor` | Diagnose an install: integrity across all skills, profile presence, installed-vs-latest version |
26
+ | `remove <skill>` | Uninstall a skill from the selected agent directories |
17
27
 
18
28
  ## Flags
19
29
 
20
30
  | Flag | Meaning |
21
31
  |---|---|
22
32
  | `--project` / `--global` | Install into the current project (default) or the user's home directory |
23
- | `--agent codex\|claude\|both` | Target agent runtime(s) |
33
+ | `--agent codex\|claude\|hermes\|both\|all` | Target agent runtime(s); `both` = codex+claude |
24
34
  | `--dir PATH` | Operate on a different project directory |
25
35
  | `--dry-run` | Show what would change without writing |
26
36
  | `--force` | Overwrite a modified installed copy |
27
37
  | `--non-interactive` | Skip prompts and use defaults: `--agent both`, project-local (for automation) |
28
38
 
39
+ ## Agent surfaces
40
+
41
+ | Agent | Project install | Global install |
42
+ |---|---|---|
43
+ | Claude Code | `.claude/skills/` | `~/.claude/skills/` |
44
+ | Codex | `.agents/skills/` | `~/.agents/skills/` |
45
+ | Hermes | — (Hermes discovers skills globally only) | `~/.hermes/skills/` |
46
+
47
+ `--agent hermes` therefore requires `--global`; `--agent all` on a project install covers codex+claude and prints a notice about hermes.
48
+
29
49
  ## Integrity model
30
50
 
31
51
  The package ships a checksum manifest that is verified at install and by `verify` — it proves the installed bytes match what was packed, not who published it. Publisher identity is attested separately by npm provenance, generated by the trusted-publishing release pipeline. Verify it with `npm audit signatures` or on the package's npm page.
package/dist/index.js CHANGED
@@ -10,25 +10,32 @@ import { fileURLToPath } from "node:url";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { spawnSync } from "node:child_process";
12
12
  var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
13
- var skillName = "vegastack-arch-guardian";
14
- var surfaces = { codex: ".agents/skills", claude: ".claude/skills" };
13
+ var bundleRoot = join(packageRoot, "skill");
14
+ var surfaces = { codex: ".agents/skills", claude: ".claude/skills", hermes: ".hermes/skills" };
15
15
  var packageVersion = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")).version;
16
16
  function usage() {
17
- return `Usage: vegastack-skills <add|verify|doctor|remove> [vegastack-arch-guardian] [options]
17
+ return `Usage: vegastack-skills <add|verify|remove> <skill> [options]
18
+ vegastack-skills <list|doctor> [options]
18
19
 
19
20
  Options:
20
- --agent codex|claude|both
21
+ --agent codex|claude|hermes|both|all (both = codex+claude; hermes is global-only)
21
22
  --project | --global
22
23
  --dir PATH
23
24
  --dry-run
24
25
  --force
25
26
  --non-interactive
26
27
  --version
28
+
29
+ Run "vegastack-skills list" to see the bundled skills.
27
30
  `;
28
31
  }
32
+ async function bundledSkills() {
33
+ const entries = await readdir(bundleRoot, { withFileTypes: true });
34
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
35
+ }
29
36
  function parse(argv) {
30
37
  const command = argv[0] && !argv[0].startsWith("-") ? argv.shift() : "help";
31
- const options = { command, skill: skillName, dryRun: false, force: false, nonInteractive: false };
38
+ const options = { command, dryRun: false, force: false, nonInteractive: false };
32
39
  if (argv[0] && !argv[0].startsWith("-"))
33
40
  options.skill = argv.shift();
34
41
  while (argv.length) {
@@ -54,16 +61,22 @@ function parse(argv) {
54
61
  else
55
62
  throw new Error(`Unknown option: ${flag}`);
56
63
  }
57
- if (!["add", "verify", "doctor", "remove", "version", "help"].includes(options.command))
64
+ if (!["add", "verify", "doctor", "remove", "list", "version", "help"].includes(options.command))
58
65
  throw new Error(`Unknown command: ${options.command}`);
59
- if (options.skill !== skillName)
60
- throw new Error(`Unknown skill: ${options.skill}`);
61
- if (options.agent && !["codex", "claude", "both"].includes(options.agent))
66
+ if (options.agent && !["codex", "claude", "hermes", "both", "all"].includes(options.agent))
62
67
  throw new Error(`Invalid --agent: ${options.agent}`);
63
68
  if (options.mode === "global" && options.dir)
64
69
  throw new Error("--dir cannot be combined with --global");
65
70
  return options;
66
71
  }
72
+ async function requireSkill(options) {
73
+ const skills = await bundledSkills();
74
+ if (!options.skill)
75
+ throw new Error(`Specify a skill: ${skills.join(", ")}`);
76
+ if (!skills.includes(options.skill))
77
+ throw new Error(`Unknown skill: ${options.skill}. Bundled skills: ${skills.join(", ")}`);
78
+ return options.skill;
79
+ }
67
80
  async function prompt(options) {
68
81
  if (options.agent && options.mode)
69
82
  return { agent: options.agent, mode: options.mode };
@@ -71,15 +84,25 @@ async function prompt(options) {
71
84
  return { agent: options.agent ?? "both", mode: options.mode ?? "project" };
72
85
  }
73
86
  const rl = createInterface({ input: process.stdin, output: process.stdout });
74
- const agentAnswer = options.agent ?? await rl.question("Install for codex, claude, or both? [both] ");
87
+ const agentAnswer = options.agent ?? await rl.question("Install for codex, claude, hermes, both (codex+claude), or all? [both] ");
75
88
  const modeAnswer = options.mode ?? await rl.question("Install project-local or user-global? [project] ");
76
89
  rl.close();
77
90
  const agent = agentAnswer || "both";
78
91
  const mode = modeAnswer === "global" ? "global" : "project";
79
- if (!["codex", "claude", "both"].includes(agent))
92
+ if (!["codex", "claude", "hermes", "both", "all"].includes(agent))
80
93
  throw new Error(`Invalid agent choice: ${agent}`);
81
94
  return { agent, mode };
82
95
  }
96
+ function resolveAgents(choice, mode) {
97
+ const wanted = choice === "both" ? ["codex", "claude"] : choice === "all" ? ["codex", "claude", "hermes"] : [choice];
98
+ if (mode === "project" && wanted.includes("hermes")) {
99
+ if (choice === "hermes")
100
+ throw new Error("Hermes discovers skills only in the global ~/.hermes/skills directory; use --global (without --dir) with --agent hermes, or --agent all");
101
+ console.log("note: skipping hermes for a project install — Hermes discovers skills globally only; run with --global --agent hermes");
102
+ return wanted.filter((agent) => agent !== "hermes");
103
+ }
104
+ return wanted;
105
+ }
83
106
  async function exists(path) {
84
107
  try {
85
108
  await lstat(path);
@@ -140,24 +163,29 @@ async function recoverInstall(base) {
140
163
  return;
141
164
  await assertNoSymlink(journalPath, false);
142
165
  const journal = JSON.parse(await readFile(journalPath, "utf8"));
143
- if (journal.schemaVersion !== 1 || !["prepared", "committed"].includes(journal.status) || !Array.isArray(journal.operations))
144
- throw new Error(`Invalid installer recovery journal: ${journalPath}`);
145
- const seenAgents = new Set;
166
+ if (journal.schemaVersion !== 2 || !["prepared", "committed"].includes(journal.status) || !Array.isArray(journal.operations)) {
167
+ throw new Error(`Unsupported installer recovery journal (schemaVersion ${journal.schemaVersion ?? "unknown"}); inspect and remove it manually: ${journalPath}`);
168
+ }
169
+ const skills = new Set(await bundledSkills());
170
+ const seen = new Set;
146
171
  for (const operation of journal.operations) {
147
- if (!["codex", "claude"].includes(operation.agent) || seenAgents.has(operation.agent))
148
- throw new Error(`Untrusted installer recovery journal: invalid agent`);
149
- seenAgents.add(operation.agent);
150
- const expectedDestination = join(base, surfaces[operation.agent], skillName);
172
+ if (!["codex", "claude", "hermes"].includes(operation.agent) || typeof operation.skill !== "string" || !skills.has(operation.skill))
173
+ throw new Error(`Untrusted installer recovery journal: invalid agent or skill; inspect and remove it manually: ${journalPath}`);
174
+ const key = `${operation.agent}/${operation.skill}`;
175
+ if (seen.has(key))
176
+ throw new Error("Untrusted installer recovery journal: duplicate operation");
177
+ seen.add(key);
178
+ const expectedDestination = join(base, surfaces[operation.agent], operation.skill);
151
179
  if (resolve(operation.destination) !== expectedDestination || typeof operation.existed !== "boolean")
152
- throw new Error(`Untrusted installer recovery journal: destination outside installer roots`);
180
+ throw new Error("Untrusted installer recovery journal: destination outside installer roots");
153
181
  const expectedParent = dirname(expectedDestination);
154
182
  const validateTemporary = (path, kind) => {
155
183
  if (!path)
156
184
  return kind === "backup" && !operation.existed;
157
- return dirname(resolve(path)) === expectedParent && basename(path).startsWith(`.${skillName}.${kind}-`);
185
+ return dirname(resolve(path)) === expectedParent && basename(path).startsWith(`.${operation.skill}.${kind}-`);
158
186
  };
159
187
  if (!validateTemporary(operation.stage, "stage") || !validateTemporary(operation.backup, "backup"))
160
- throw new Error(`Untrusted installer recovery journal: invalid transaction path`);
188
+ throw new Error("Untrusted installer recovery journal: invalid transaction path");
161
189
  }
162
190
  for (const operation of [...journal.operations].reverse()) {
163
191
  for (const path of [operation.destination, operation.stage, operation.backup].filter(Boolean))
@@ -237,76 +265,89 @@ async function listFiles(root) {
237
265
  return output.sort();
238
266
  }
239
267
  var hash = (body) => createHash("sha256").update(body).digest("hex");
240
- async function loadSource() {
241
- const source = join(packageRoot, "skill", skillName);
242
- const integrityPath = join(packageRoot, "skill-integrity.json");
243
- const manifest = JSON.parse(await readFile(integrityPath, "utf8"));
244
- if (manifest.schemaVersion !== 1 || manifest.skill !== skillName)
268
+ async function loadManifest() {
269
+ const manifest = JSON.parse(await readFile(join(packageRoot, "skill-integrity.json"), "utf8"));
270
+ if (manifest.schemaVersion !== 2 || typeof manifest.skills !== "object")
245
271
  throw new Error("Invalid bundled skill manifest");
272
+ return manifest;
273
+ }
274
+ async function loadSource(skillName) {
275
+ const manifest = await loadManifest();
276
+ const skillManifest = manifest.skills[skillName];
277
+ if (!skillManifest)
278
+ throw new Error(`Bundled manifest has no entry for skill ${skillName}`);
279
+ const source = join(bundleRoot, skillName);
246
280
  const skill = await readFile(join(source, "SKILL.md"), "utf8");
247
281
  if (!skill.startsWith(`---
248
- `) || !/^name: vegastack-arch-guardian$/m.test(skill) || !/^description: .+/m.test(skill)) {
249
- throw new Error("Bundled skill fails Agent Skills frontmatter validation");
282
+ `) || !new RegExp(`^name: ${skillName}$`, "m").test(skill) || !/^description: .+/m.test(skill)) {
283
+ throw new Error(`Bundled skill ${skillName} fails Agent Skills frontmatter validation`);
250
284
  }
251
285
  const observed = {};
252
286
  for (const file of await listFiles(source))
253
287
  observed[relative(source, file).split(sep).join("/")] = hash(await readFile(file));
254
- if (JSON.stringify(observed) !== JSON.stringify(manifest.files))
255
- throw new Error("Bundled skill checksum mismatch");
256
- return { source, manifest };
288
+ if (JSON.stringify(observed) !== JSON.stringify(skillManifest.files))
289
+ throw new Error(`Bundled skill ${skillName} checksum mismatch`);
290
+ return { source, files: skillManifest.files };
257
291
  }
258
- function destinations(agent, mode, directory) {
259
- const agents = agent === "both" ? ["codex", "claude"] : [agent];
260
- const base = mode === "global" ? homedir() : resolve(directory ?? process.cwd());
261
- return agents.map((item) => ({ agent: item, destination: join(base, surfaces[item], skillName) }));
292
+ function baseFor(mode, directory) {
293
+ return mode === "global" ? homedir() : resolve(directory ?? process.cwd());
262
294
  }
263
- async function compare(destination, manifest) {
295
+ async function compare(destination, files) {
264
296
  if (!await exists(destination))
265
297
  return { status: "missing", issues: ["not installed"] };
266
298
  await assertNoSymlink(destination, false);
267
299
  const issues = [];
268
300
  const actualFiles = await listFiles(destination);
269
301
  const actualKeys = new Set(actualFiles.map((file) => relative(destination, file).split(sep).join("/")).filter((key) => key !== ".vegastack-install.json"));
270
- for (const [key, expected] of Object.entries(manifest.files)) {
302
+ for (const [key, expected] of Object.entries(files)) {
271
303
  if (!actualKeys.has(key))
272
304
  issues.push(`missing ${key}`);
273
305
  else if (hash(await readFile(join(destination, key))) !== expected)
274
306
  issues.push(`changed ${key}`);
275
307
  }
276
308
  for (const key of actualKeys)
277
- if (!(key in manifest.files))
309
+ if (!(key in files))
278
310
  issues.push(`unexpected ${key}`);
279
311
  return { status: issues.length ? "drifted" : "verified", issues };
280
312
  }
281
313
  async function install(options) {
314
+ const skillName = await requireSkill(options);
282
315
  const choice = await prompt(options);
283
- const base = choice.mode === "global" ? homedir() : resolve(options.dir ?? process.cwd());
316
+ const base = baseFor(choice.mode, options.dir);
317
+ const agents = resolveAgents(choice.agent, choice.mode);
318
+ if (!agents.length)
319
+ return;
284
320
  if (!options.dryRun)
285
- return withInstallLock(base, () => installLocked(options, choice, base));
286
- return installLocked(options, choice, base, false);
321
+ return withInstallLock(base, () => installLocked(options, skillName, agents, base));
322
+ return installLocked(options, skillName, agents, base, false);
287
323
  }
288
- async function installLocked(options, choice, base, recover = true) {
324
+ async function installLocked(options, skillName, agents, base, recover = true) {
289
325
  if (recover)
290
326
  await recoverInstall(base);
291
- const { source, manifest } = await loadSource();
292
- const targets = destinations(choice.agent, choice.mode, options.dir);
327
+ const { source, files } = await loadSource(skillName);
293
328
  const operations = [];
294
- for (const target of targets) {
295
- await assertNoSymlink(target.destination);
296
- const parent = dirname(target.destination);
329
+ for (const agent of agents) {
330
+ const destination = join(base, surfaces[agent], skillName);
331
+ await assertNoSymlink(destination);
332
+ const parent = dirname(destination);
297
333
  await assertNoSymlink(parent);
298
- const existed = await exists(target.destination);
334
+ const existed = await exists(destination);
299
335
  if (existed) {
300
- const comparison = await compare(target.destination, manifest);
336
+ const comparison = await compare(destination, files);
301
337
  if (comparison.status === "verified" && !options.force) {
302
- console.log(`unchanged ${target.agent}: ${target.destination}`);
338
+ console.log(`unchanged ${agent}: ${destination}`);
303
339
  continue;
304
340
  }
305
- if (!options.force)
306
- throw new Error(`Refusing differing installation without --force: ${target.destination}`);
341
+ if (!options.force) {
342
+ if (options.dryRun) {
343
+ console.log(`would replace ${agent} (requires --force; installed copy differs): ${destination}`);
344
+ continue;
345
+ }
346
+ throw new Error(`Refusing differing installation without --force: ${destination}`);
347
+ }
307
348
  }
308
349
  const suffix = randomUUID();
309
- operations.push({ ...target, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
350
+ operations.push({ skill: skillName, agent, destination, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
310
351
  }
311
352
  if (options.dryRun) {
312
353
  for (const operation of operations)
@@ -323,14 +364,14 @@ async function installLocked(options, choice, base, recover = true) {
323
364
  await mkdir(dirname(operation.destination), { recursive: true });
324
365
  await assertNoSymlink(dirname(operation.destination), false);
325
366
  await cp(source, operation.stage, { recursive: true, dereference: false, errorOnExist: true });
326
- await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, manifest }, null, 2)}
367
+ await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, skill: skillName, files }, null, 2)}
327
368
  `, { flag: "wx" });
328
- const stagedCheck = await compare(operation.stage, manifest);
369
+ const stagedCheck = await compare(operation.stage, files);
329
370
  if (stagedCheck.status !== "verified")
330
371
  throw new Error(`Staged copy failed verification: ${stagedCheck.issues.join(", ")}`);
331
372
  staged.push(operation);
332
373
  }
333
- await durableJson(journalPath, { schemaVersion: 1, status: "prepared", operations });
374
+ await durableJson(journalPath, { schemaVersion: 2, status: "prepared", operations });
334
375
  for (const operation of operations) {
335
376
  await assertNoSymlink(dirname(operation.destination), false);
336
377
  if (await exists(operation.destination))
@@ -346,7 +387,7 @@ async function installLocked(options, choice, base, recover = true) {
346
387
  }
347
388
  applied.push(operation);
348
389
  }
349
- await durableJson(journalPath, { schemaVersion: 1, status: "committed", operations });
390
+ await durableJson(journalPath, { schemaVersion: 2, status: "committed", operations });
350
391
  } catch (error) {
351
392
  if (await exists(journalPath))
352
393
  await recoverInstall(base);
@@ -375,17 +416,44 @@ async function installLocked(options, choice, base, recover = true) {
375
416
  }
376
417
  async function verify(options) {
377
418
  const choice = await prompt(options);
378
- const { manifest } = await loadSource();
419
+ const base = baseFor(choice.mode, options.dir);
420
+ const agents = resolveAgents(choice.agent, choice.mode);
421
+ const explicit = Boolean(options.skill);
422
+ const skills = explicit ? [await requireSkill(options)] : await bundledSkills();
379
423
  let failed = false;
380
- for (const target of destinations(choice.agent, choice.mode, options.dir)) {
381
- const result = await compare(target.destination, manifest);
382
- console.log(`${result.status} ${target.agent}: ${target.destination}${result.issues.length ? ` (${result.issues.join(", ")})` : ""}`);
383
- if (result.status !== "verified")
384
- failed = true;
424
+ let found = 0;
425
+ for (const skillName of skills) {
426
+ const { files } = await loadSource(skillName);
427
+ for (const agent of agents) {
428
+ const destination = join(base, surfaces[agent], skillName);
429
+ const result = await compare(destination, files);
430
+ if (result.status === "missing" && !explicit) {
431
+ console.log(`not installed ${agent} ${skillName}`);
432
+ continue;
433
+ }
434
+ found += 1;
435
+ console.log(`${result.status} ${agent} ${skillName}: ${destination}${result.issues.length ? ` (${result.issues.join(", ")})` : ""}`);
436
+ if (result.status !== "verified")
437
+ failed = true;
438
+ }
439
+ }
440
+ if (!explicit && found === 0) {
441
+ console.log("no bundled skills are installed on the selected surfaces");
442
+ failed = true;
385
443
  }
386
444
  if (failed)
387
445
  process.exitCode = 1;
388
446
  }
447
+ function semverLess(a, b) {
448
+ const parse2 = (value) => value.split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
449
+ const [aMajor = 0, aMinor = 0, aPatch = 0] = parse2(a);
450
+ const [bMajor = 0, bMinor = 0, bPatch = 0] = parse2(b);
451
+ if (aMajor !== bMajor)
452
+ return aMajor < bMajor;
453
+ if (aMinor !== bMinor)
454
+ return aMinor < bMinor;
455
+ return aPatch < bPatch;
456
+ }
389
457
  async function latestPublishedVersion() {
390
458
  try {
391
459
  const response = await fetch("https://registry.npmjs.org/@vegastack%2fskills/latest", { signal: AbortSignal.timeout(3000) });
@@ -398,37 +466,57 @@ async function latestPublishedVersion() {
398
466
  }
399
467
  }
400
468
  async function removeSkill(options) {
469
+ const skillName = await requireSkill(options);
401
470
  const choice = await prompt(options);
471
+ const base = baseFor(choice.mode, options.dir);
472
+ const agents = resolveAgents(choice.agent, choice.mode);
473
+ if (!options.dryRun)
474
+ return withInstallLock(base, () => removeLocked(options, skillName, agents, base));
475
+ return removeLocked(options, skillName, agents, base);
476
+ }
477
+ async function removeLocked(options, skillName, agents, base) {
402
478
  let removed = 0;
403
- for (const target of destinations(choice.agent, choice.mode, options.dir)) {
404
- if (!await exists(target.destination)) {
405
- console.log(`not installed ${target.agent}: ${target.destination}`);
479
+ for (const agent of agents) {
480
+ const destination = join(base, surfaces[agent], skillName);
481
+ if (!await exists(destination)) {
482
+ console.log(`not installed ${agent}: ${destination}`);
406
483
  continue;
407
484
  }
408
- await assertNoSymlink(target.destination, false);
485
+ await assertNoSymlink(destination, false);
409
486
  if (!options.force) {
410
- const { manifest } = await loadSource();
411
- const comparison = await compare(target.destination, manifest);
487
+ const { files } = await loadSource(skillName);
488
+ const comparison = await compare(destination, files);
412
489
  if (comparison.status === "drifted")
413
- throw new Error(`Installation differs from the bundled skill (possibly locally modified); re-run with --force to remove anyway: ${target.destination}`);
490
+ throw new Error(`Installation differs from the bundled skill (possibly locally modified); re-run with --force to remove anyway: ${destination}`);
414
491
  }
415
492
  if (options.dryRun) {
416
- console.log(`would remove ${target.agent}: ${target.destination}`);
493
+ console.log(`would remove ${agent}: ${destination}`);
417
494
  continue;
418
495
  }
419
- await rm(target.destination, { recursive: true, force: true });
496
+ await rm(destination, { recursive: true, force: true });
420
497
  removed += 1;
421
- console.log(`removed ${target.agent}: ${target.destination}`);
498
+ console.log(`removed ${agent}: ${destination}`);
422
499
  }
423
500
  if (!removed && !options.dryRun)
424
501
  process.exitCode = 1;
425
502
  }
503
+ async function list() {
504
+ const manifest = await loadManifest();
505
+ for (const skillName of await bundledSkills()) {
506
+ const skill = await readFile(join(bundleRoot, skillName, "SKILL.md"), "utf8");
507
+ const description = skill.match(/^description: (.+)$/m)?.[1] ?? "";
508
+ const fileCount = Object.keys(manifest.skills[skillName]?.files ?? {}).length;
509
+ console.log(`${skillName} (${fileCount} files)`);
510
+ console.log(` ${description.length > 160 ? `${description.slice(0, 157)}...` : description}`);
511
+ }
512
+ }
426
513
  async function doctor(options) {
427
- const base = options.mode === "global" ? homedir() : resolve(options.dir ?? process.cwd());
514
+ const base = baseFor(options.mode ?? "project", options.dir);
428
515
  await access(base, fsConstants.R_OK | fsConstants.W_OK);
429
516
  await assertNoSymlink(base, false);
430
517
  let failed = false;
431
518
  const profileCandidates = [join(base, ".vegastack", "architecture.json"), join(base, ".vegastack", "architecture.yaml")];
519
+ let profileFound = false;
432
520
  if (options.mode !== "global") {
433
521
  let profile = null;
434
522
  for (const candidate of profileCandidates)
@@ -436,6 +524,7 @@ async function doctor(options) {
436
524
  profile = candidate;
437
525
  break;
438
526
  }
527
+ profileFound = Boolean(profile);
439
528
  if (profile) {
440
529
  if (profile.endsWith(".yaml"))
441
530
  console.log(`notice: ${profile} uses the legacy .yaml name for a JSON document; rename to architecture.json`);
@@ -449,39 +538,44 @@ async function doctor(options) {
449
538
  failed = true;
450
539
  }
451
540
  } else {
452
- console.log(`missing architecture profile: ${profileCandidates[0]}`);
453
- failed = true;
541
+ console.log(`missing architecture profile: ${profileCandidates[0]} (only needed for arch-guardian checks)`);
454
542
  }
455
543
  }
456
544
  console.log(`ok runtime: Node ${process.versions.node}`);
457
545
  const latest = await latestPublishedVersion();
458
- if (latest && latest !== packageVersion)
459
- console.log(`update available: installed ${packageVersion}, latest ${latest} — run: npx @vegastack/skills@latest add ${skillName} --force`);
546
+ if (latest && semverLess(packageVersion, latest))
547
+ console.log(`update available: installed ${packageVersion}, latest ${latest} — run: npx @vegastack/skills@latest add <skill> --force`);
548
+ else if (latest && semverLess(latest, packageVersion))
549
+ console.log(`ok installer version: ${packageVersion} (ahead of registry latest ${latest})`);
460
550
  else if (latest)
461
551
  console.log(`ok installer version: ${packageVersion} (latest)`);
462
552
  else
463
553
  console.log(`skipped installer version check (npmjs.org unreachable); installed ${packageVersion}`);
464
- const { manifest } = await loadSource();
465
554
  let installations = 0;
466
- let checkScript = "";
467
- for (const agent of ["codex", "claude"]) {
468
- const destination = join(base, surfaces[agent], skillName);
469
- if (!await exists(destination)) {
470
- console.log(`missing ${agent} guardian installation`);
471
- continue;
555
+ const checkScripts = [];
556
+ for (const skillName of await bundledSkills()) {
557
+ const { files } = await loadSource(skillName);
558
+ for (const agent of ["codex", "claude", "hermes"]) {
559
+ const destination = join(agent === "hermes" ? homedir() : base, surfaces[agent], skillName);
560
+ if (!await exists(destination))
561
+ continue;
562
+ installations += 1;
563
+ const result = await compare(destination, files);
564
+ console.log(`${result.status === "verified" ? "ok" : "invalid"} ${agent} ${skillName} installation${result.issues.length ? ` (${result.issues.join(", ")})` : ""}`);
565
+ if (result.status !== "verified")
566
+ failed = true;
567
+ const candidate = join(destination, "scripts", "architecture-check.mjs");
568
+ if (await exists(candidate))
569
+ checkScripts.push(candidate);
472
570
  }
473
- installations += 1;
474
- const result = await compare(destination, manifest);
475
- console.log(`${result.status === "verified" ? "ok" : "invalid"} ${agent} guardian installation${result.issues.length ? ` (${result.issues.join(", ")})` : ""}`);
476
- if (result.status !== "verified")
477
- failed = true;
478
- if (!checkScript)
479
- checkScript = join(destination, "scripts", "architecture-check.mjs");
480
571
  }
481
- if (!installations)
572
+ if (!installations) {
573
+ console.log("no bundled skills installed on any surface");
482
574
  failed = true;
483
- if (options.mode !== "global" && checkScript && !failed) {
484
- const result = spawnSync(process.execPath, [checkScript, base, "--json"], { encoding: "utf8" });
575
+ }
576
+ const firstCheckScript = checkScripts[0];
577
+ if (options.mode !== "global" && firstCheckScript && profileFound && !failed) {
578
+ const result = spawnSync(process.execPath, [firstCheckScript, base, "--json"], { encoding: "utf8" });
485
579
  if (result.status === 0)
486
580
  console.log("ok architecture invariants");
487
581
  else {
@@ -498,6 +592,8 @@ async function main() {
498
592
  return console.log(usage());
499
593
  if (options.command === "version")
500
594
  return console.log(packageVersion);
595
+ if (options.command === "list")
596
+ return list();
501
597
  if (options.command === "add")
502
598
  return install(options);
503
599
  if (options.command === "verify")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vegastack/skills",
3
- "version": "0.1.0",
4
- "description": "Installer and verifier for VegaStack Agent Skills (Claude Code and Codex)",
3
+ "version": "0.2.0",
4
+ "description": "Installer and verifier for VegaStack Agent Skills (Claude Code, Codex, and Hermes)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -16,13 +16,24 @@
16
16
  "agent-skills",
17
17
  "claude-code",
18
18
  "codex",
19
+ "hermes",
19
20
  "skill",
20
21
  "architecture",
21
22
  "vegastack"
22
23
  ],
23
- "bin": { "vegastack-skills": "dist/index.js" },
24
- "files": ["dist", "skill", "skill-integrity.json", "README.md", "LICENSE"],
25
- "engines": { "node": ">=20.11" },
24
+ "bin": {
25
+ "vegastack-skills": "dist/index.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "skill",
30
+ "skill-integrity.json",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20.11"
36
+ },
26
37
  "scripts": {
27
38
  "sync": "bun scripts/sync-skill.mjs",
28
39
  "build": "bun run sync && bun build src/index.ts --target=node --outfile dist/index.js && chmod +x dist/index.js",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: vegastack-arch-guardian
2
+ name: arch-guardian
3
3
  description: Architecture advisor for VegaStack projects. Use when designing a new service or feature, reviewing architecture or a risky change (auth, tenancy, agents, jobs, connectors, data lifecycle, hosting), deciding between architectural options, writing or validating an ADR or .vegastack/architecture.json profile, threat modeling, planning a migration, or checking dependency and source drift. Gives one scoped recommendation with an explicit verdict; activates only the capabilities a project has actually enabled. Covers web-only, Flutter, agentic and non-agentic, single- and multi-tenant, internal, public, platform-service, and shared-package projects.
4
4
  ---
5
5
 
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "VegaStack Architecture Guardian"
3
+ short_description: "Design and guard adaptive VegaStack architecture"
4
+ default_prompt: "Use $arch-guardian to make one scoped architecture recommendation for this project without assuming absent capabilities."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/vegastack/vegastack-skills/main/skills/vegastack-arch-guardian/assets/architecture-profile.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/vegastack/vegastack-skills/main/skills/arch-guardian/assets/architecture-profile.schema.json",
4
4
  "title": "VegaStack Architecture Profile v3",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -1,4 +1,4 @@
1
- # Refresh contract — vegastack-arch-guardian
1
+ # Refresh contract — arch-guardian
2
2
 
3
3
  Instructions for the scheduled refresh agent (and any human running a manual refresh). This file, together with `sources.json`, is the complete freshness contract for this skill.
4
4