@vegastack/skills 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,16 +3,19 @@
3
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 list
7
- npx @vegastack/skills add dev-architect
6
+ npx @vegastack/skills list # what is bundled, by group
7
+ npx @vegastack/skills add --group dev-skills # the whole dev workflow, one command
8
+ npx @vegastack/skills add dev-architect # or a single skill
8
9
  ```
9
10
 
10
11
  ## Skills in this package
11
12
 
13
+ ### `dev-skills` — the issue-driven dev workflow
14
+
15
+ Install the family with `add --group dev-skills`.
16
+
12
17
  | Skill | What it does |
13
18
  |---|---|
14
- | `skill-maintainer` | Encodes the Agent Skills standards (Claude Code, Codex, Hermes, agentskills.io) for creating, updating, and releasing skills in a skills repo |
15
- | `skillify` | Turns a feature or workflow into a complete skill conforming to the VegaStack skills contract, or audits an existing one |
16
19
  | `dev-setup` | Bootstraps any project, greenfield included, for the issue-driven dev workflow: stack-playbook-drafted profile, AGENTS.md section, labels, guards, decision register |
17
20
  | `dev-intake` | Turns ideas, brainstorms, and SOWs into agent-ready GitHub issues with recorded user approval |
18
21
  | `dev-plan` | Plans an approved issue before any code exists: fresh-grounded questionnaire, strict plan format with Interfaces blocks, the scope ratchet, quick-build inline mode |
@@ -24,26 +27,60 @@ npx @vegastack/skills add dev-architect
24
27
  | `dev-status` | The operator's board: whose move is it, from deterministic gh data |
25
28
  | `dev-chronicle` | The project's narrative record: story entries per branch and the "catch me up" digest |
26
29
 
30
+ ### `repo-tooling` — repo-only
31
+
32
+ These operate on the vegastack-skills repository itself and do nothing useful in another project, so **`--all` skips them**. Install one by name if you are contributing to that repo.
33
+
34
+ | Skill | What it does |
35
+ |---|---|
36
+ | `skill-maintainer` | Encodes the Agent Skills standards (Claude Code, Codex, Hermes, agentskills.io) for creating, updating, and releasing skills in a skills repo |
37
+ | `skillify` | Turns a feature or workflow into a complete skill conforming to the VegaStack skills contract, or audits an existing one |
38
+
27
39
  ## Commands
28
40
 
29
41
  | Command | What it does |
30
42
  |---|---|
31
43
  | `list` | Show the bundled skills |
32
- | `add <skill>` | Install (or upgrade) a skill into the selected agent directories |
33
- | `verify [skill]` | Check installed copies against the bundled checksum manifest (all bundled skills when no name given) |
44
+ | `add <selection>` | Install (or upgrade) skills into the selected agent directories |
45
+ | `verify [selection]` | Check installed copies against the bundled checksum manifest (all bundled skills when nothing is selected) |
34
46
  | `doctor` | Diagnose an install: integrity across all skills, dev profile (`.vegastack/dev.md`) presence, installed-vs-latest version |
35
- | `remove <skill>` | Uninstall a skill from the selected agent directories |
47
+ | `remove <selection>` | Uninstall skills from the selected agent directories |
48
+
49
+ ### Selecting what to act on
50
+
51
+ `add`, `verify`, and `remove` each take **exactly one** selector. Combining two is an error, not a merge.
52
+
53
+ | Selector | Means |
54
+ |---|---|
55
+ | `<skill>` | That one skill. Works for every bundled skill, repo-only ones included |
56
+ | `--group <group>` | Every skill in that group |
57
+ | `--all` | Every bundled skill **except** the repo-only ones |
58
+
59
+ A `--group` or `--all` install is **one transaction**: every skill is checked and staged before any of them is committed, so if one fails, none are installed and the destination is left exactly as it was.
60
+
61
+ ```sh
62
+ npx @vegastack/skills add --group dev-skills # the ten dev-workflow skills
63
+ npx @vegastack/skills add --all # everything worth installing in your project
64
+ npx @vegastack/skills verify --group dev-skills # check the family against the manifest
65
+ npx @vegastack/skills remove --group dev-skills # uninstall it again
66
+ ```
36
67
 
37
68
  ## Flags
38
69
 
39
70
  | Flag | Meaning |
40
71
  |---|---|
41
72
  | `--project` / `--global` | Install into the current project (default) or the user's home directory |
73
+ | `--group NAME` | Select every skill in a group (see `list` for the groups) |
74
+ | `--all` | Select every bundled skill except the repo-only ones |
42
75
  | `--agent codex\|claude\|hermes\|both\|all` | Target agent runtime(s); `both` = codex+claude |
43
- | `--dir PATH` | Operate on a different project directory |
76
+ | `--dir PATH` | Operate on a different project directory; not valid with `--global` |
44
77
  | `--dry-run` | Show what would change without writing |
45
78
  | `--force` | Overwrite a modified installed copy |
46
79
  | `--non-interactive` | Skip prompts and use defaults: `--agent both`, project-local (for automation) |
80
+ | `--version` / `-v` | Print the installer version |
81
+ | `--help` / `-h` | Print usage |
82
+
83
+ `--all` and `--agent all` are different axes and are easy to confuse: `--all` chooses **which skills**, `--agent all` chooses **which agent runtimes**. `add --all --agent all --global` is valid and means every installable skill, on every runtime, in your home directory.
47
84
 
48
85
  Agent targeting is automatic: the CLI detects which agents you have (`~/.claude`, `~/.codex`/`~/.agents`, `~/.hermes`) and targets them without asking — `--agent` overrides. A numbered picker appears only when nothing is detected. Installs are project-local by default; pass `--global` for the home directory (required for Hermes).
49
86
 
package/dist/index.js CHANGED
@@ -3,20 +3,69 @@
3
3
  // src/index.ts
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { constants as fsConstants } from "node:fs";
6
- import { access, cp, lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
6
+ import { access, cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
8
8
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createInterface } from "node:readline/promises";
11
+
12
+ // src/selection.ts
13
+ var sorted = (names) => [...new Set(names)].sort();
14
+ function groupsIn(catalog) {
15
+ return sorted(catalog.map((entry) => entry.group).filter((group) => group !== null));
16
+ }
17
+ function selectSkills(selector, catalog, verb = "install") {
18
+ const chosen = [
19
+ selector.skill ? "a skill name" : null,
20
+ selector.group ? "--group" : null,
21
+ selector.all ? "--all" : null
22
+ ].filter(Boolean);
23
+ if (chosen.length > 1) {
24
+ throw new Error(`Use only one of ${chosen.join(", ")} — they select different things and are not combined`);
25
+ }
26
+ if (selector.all) {
27
+ const installable = sorted(catalog.filter((entry) => !entry.repoOnly).map((entry) => entry.name));
28
+ if (!installable.length) {
29
+ throw new Error("Nothing to install: every bundled skill is repo-only. Name one explicitly, or use --group, to install it anyway.");
30
+ }
31
+ return installable;
32
+ }
33
+ if (selector.group) {
34
+ const members = sorted(catalog.filter((entry) => entry.group === selector.group).map((entry) => entry.name));
35
+ if (!members.length) {
36
+ const groups2 = groupsIn(catalog);
37
+ throw new Error(`Unknown group: ${selector.group}. Available groups: ${groups2.length ? groups2.join(", ") : "(none)"}`);
38
+ }
39
+ return members;
40
+ }
41
+ if (selector.skill) {
42
+ if (!catalog.some((entry) => entry.name === selector.skill)) {
43
+ throw new Error(`Unknown skill: ${selector.skill}. Bundled skills: ${sorted(catalog.map((entry) => entry.name)).join(", ")}`);
44
+ }
45
+ return [selector.skill];
46
+ }
47
+ const groups = groupsIn(catalog);
48
+ throw new Error(`Specify what to ${verb}: a skill name (${sorted(catalog.map((entry) => entry.name)).join(", ")})` + `${groups.length ? `, --group (${groups.join(", ")})` : ""}, or --all`);
49
+ }
50
+
51
+ // src/index.ts
11
52
  var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
53
  var bundleRoot = join(packageRoot, "skill");
13
54
  var surfaces = { codex: ".agents/skills", claude: ".claude/skills", hermes: ".hermes/skills" };
14
55
  var packageVersion = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")).version;
15
56
  function usage() {
16
57
  return `Usage: vegastack-skills <add|verify|remove> <skill> [options]
58
+ vegastack-skills <add|verify|remove> --group <group> [options]
59
+ vegastack-skills <add|verify|remove> --all [options]
17
60
  vegastack-skills <list|doctor> [options]
18
61
 
62
+ Select exactly one of: a skill name, --group <group>, or --all.
63
+ --all installs every skill except the repo-only ones; name those explicitly.
64
+ A --group or --all install is one transaction: if any skill fails, none are installed.
65
+
19
66
  Options:
67
+ --group NAME install every skill in that group
68
+ --all every bundled skill except the repo-only ones
20
69
  --agent codex|claude|hermes|both|all (both = codex+claude; hermes is global-only)
21
70
  --project | --global
22
71
  --dir PATH
@@ -34,12 +83,21 @@ async function bundledSkills() {
34
83
  }
35
84
  function parse(argv) {
36
85
  const command = argv[0] && !argv[0].startsWith("-") ? argv.shift() : "help";
37
- const options = { command, dryRun: false, force: false, nonInteractive: false };
86
+ const options = { command, all: false, dryRun: false, force: false, nonInteractive: false };
38
87
  if (argv[0] && !argv[0].startsWith("-"))
39
88
  options.skill = argv.shift();
40
89
  while (argv.length) {
41
90
  const flag = argv.shift();
42
- if (flag === "--agent")
91
+ if (flag === "--group") {
92
+ const value = argv.shift();
93
+ if (value === undefined || value === "" || value.startsWith("-"))
94
+ throw new Error("--group requires a value");
95
+ if (options.group !== undefined)
96
+ throw new Error("--group may be given only once; select one group per run");
97
+ options.group = value;
98
+ } else if (flag === "--all")
99
+ options.all = true;
100
+ else if (flag === "--agent")
43
101
  options.agent = argv.shift();
44
102
  else if (flag === "--project")
45
103
  options.mode = "project";
@@ -68,13 +126,19 @@ function parse(argv) {
68
126
  throw new Error("--dir cannot be combined with --global");
69
127
  return options;
70
128
  }
71
- async function requireSkill(options) {
72
- const skills = await bundledSkills();
73
- if (!options.skill)
74
- throw new Error(`Specify a skill: ${skills.join(", ")}`);
75
- if (!skills.includes(options.skill))
76
- throw new Error(`Unknown skill: ${options.skill}. Bundled skills: ${skills.join(", ")}`);
77
- return options.skill;
129
+ async function skillCatalog() {
130
+ const manifest = await loadManifest();
131
+ return Object.entries(manifest.skills).map(([name, entry]) => ({
132
+ name,
133
+ group: entry.group ?? null,
134
+ repoOnly: Boolean(entry.repoOnly)
135
+ }));
136
+ }
137
+ function hasSelector(options) {
138
+ return Boolean(options.skill || options.group || options.all);
139
+ }
140
+ async function requireSelection(options, verb = "install") {
141
+ return selectSkills({ skill: options.skill, group: options.group, all: options.all }, await skillCatalog(), verb);
78
142
  }
79
143
  var agentLabels = { claude: "Claude Code", codex: "Codex", hermes: "Hermes" };
80
144
  async function detectAgents() {
@@ -330,51 +394,69 @@ async function compare(destination, files) {
330
394
  return { status: issues.length ? "drifted" : "verified", issues };
331
395
  }
332
396
  async function install(options) {
333
- const skillName = await requireSkill(options);
397
+ const skillNames = await requireSelection(options);
334
398
  const choice = await prompt(options);
335
399
  const base = baseFor(choice.mode, options.dir);
336
400
  const agents = resolveAgents(choice.agent, choice.mode);
337
401
  if (!agents.length)
338
402
  return;
339
403
  if (!options.dryRun)
340
- return withInstallLock(base, () => installLocked(options, skillName, agents, base));
341
- return installLocked(options, skillName, agents, base, false);
404
+ return withInstallLock(base, () => installLocked(options, skillNames, agents, base));
405
+ return installLocked(options, skillNames, agents, base, false);
342
406
  }
343
- async function installLocked(options, skillName, agents, base, recover = true) {
407
+ async function installLocked(options, skillNames, agents, base, recover = true) {
344
408
  if (recover)
345
409
  await recoverInstall(base);
346
- const { source, files } = await loadSource(skillName);
410
+ const sources = new Map;
411
+ for (const skillName of skillNames)
412
+ sources.set(skillName, await loadSource(skillName));
347
413
  const operations = [];
348
- for (const agent of agents) {
349
- const destination = join(base, surfaces[agent], skillName);
350
- await assertNoSymlink(destination);
351
- const parent = dirname(destination);
352
- await assertNoSymlink(parent);
353
- const existed = await exists(destination);
354
- if (existed) {
355
- const comparison = await compare(destination, files);
356
- if (comparison.status === "verified" && !options.force) {
357
- console.log(`unchanged ${agent}: ${destination}`);
358
- continue;
414
+ const refusals = [];
415
+ for (const skillName of skillNames) {
416
+ const { files } = sources.get(skillName);
417
+ for (const agent of agents) {
418
+ const destination = join(base, surfaces[agent], skillName);
419
+ await assertNoSymlink(destination);
420
+ const parent = dirname(destination);
421
+ await assertNoSymlink(parent);
422
+ const existed = await exists(destination);
423
+ if (existed && !(await stat(destination)).isDirectory()) {
424
+ throw new Error(`Refusing to install over a non-directory: ${destination} — remove it and retry`);
359
425
  }
360
- if (!options.force) {
361
- if (options.dryRun) {
362
- console.log(`would replace ${agent} (requires --force; installed copy differs): ${destination}`);
426
+ if (existed) {
427
+ const comparison = await compare(destination, files);
428
+ if (comparison.status === "verified" && !options.force) {
429
+ console.log(`unchanged ${agent}: ${destination}`);
363
430
  continue;
364
431
  }
365
- throw new Error(`Refusing differing installation without --force: ${destination}`);
432
+ if (!options.force) {
433
+ if (options.dryRun) {
434
+ refusals.push({ agent, path: destination });
435
+ continue;
436
+ }
437
+ throw new Error(`Refusing differing installation without --force: ${destination}`);
438
+ }
366
439
  }
440
+ const suffix = randomUUID();
441
+ operations.push({ skill: skillName, agent, destination, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
367
442
  }
368
- const suffix = randomUUID();
369
- operations.push({ skill: skillName, agent, destination, existed, stage: join(parent, `.${skillName}.stage-${suffix}`), backup: existed ? join(parent, `.${skillName}.backup-${suffix}`) : undefined });
370
443
  }
371
444
  if (options.dryRun) {
372
445
  for (const operation of operations)
373
446
  console.log(`would install ${operation.agent}: ${operation.destination}`);
447
+ for (const destination of refusals)
448
+ console.log(`would replace ${destination.agent} (requires --force; installed copy differs): ${destination.path}`);
449
+ if (refusals.length && skillNames.length > 1) {
450
+ console.log(`this run would install nothing: ${refusals.length} destination(s) differ and --force was not given`);
451
+ process.exitCode = 1;
452
+ }
374
453
  return;
375
454
  }
376
- if (!operations.length)
455
+ if (!operations.length) {
456
+ if (skillNames.length > 1)
457
+ console.log(`${skillNames.length} skills already installed and unchanged${options.group ? ` (${options.group})` : ""}`);
377
458
  return;
459
+ }
378
460
  const journalPath = join(base, ".vegastack", ".skills-install-transaction.json");
379
461
  const staged = [];
380
462
  const applied = [];
@@ -382,8 +464,9 @@ async function installLocked(options, skillName, agents, base, recover = true) {
382
464
  for (const operation of operations) {
383
465
  await mkdir(dirname(operation.destination), { recursive: true });
384
466
  await assertNoSymlink(dirname(operation.destination), false);
467
+ const { source, files } = sources.get(operation.skill);
385
468
  await cp(source, operation.stage, { recursive: true, dereference: false, errorOnExist: true });
386
- await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, skill: skillName, files }, null, 2)}
469
+ await writeFile(join(operation.stage, ".vegastack-install.json"), `${JSON.stringify({ installer: "@vegastack/skills", version: packageVersion, skill: operation.skill, files }, null, 2)}
387
470
  `, { flag: "wx" });
388
471
  const stagedCheck = await compare(operation.stage, files);
389
472
  if (stagedCheck.status !== "verified")
@@ -432,13 +515,21 @@ async function installLocked(options, skillName, agents, base, recover = true) {
432
515
  await syncDirectory(dirname(journalPath));
433
516
  for (const operation of applied)
434
517
  console.log(`installed ${operation.agent}: ${operation.destination}`);
518
+ if (skillNames.length > 1) {
519
+ const skipped = options.all ? (await skillCatalog()).filter((entry) => entry.repoOnly).map((entry) => entry.name) : [];
520
+ const note = skipped.length ? ` (skipped ${skipped.length} repo-only: ${skipped.join(", ")} — name one explicitly to install it)` : "";
521
+ const installedCount = new Set(applied.map((operation) => operation.skill)).size;
522
+ const unchanged = skillNames.length - installedCount;
523
+ const unchangedNote = unchanged > 0 ? `, ${unchanged} already up to date` : "";
524
+ console.log(`installed ${installedCount} skills${options.group ? ` from ${options.group}` : ""}${unchangedNote}${note}`);
525
+ }
435
526
  }
436
527
  async function verify(options) {
437
528
  const choice = await prompt(options);
438
529
  const base = baseFor(choice.mode, options.dir);
439
530
  const agents = resolveAgents(choice.agent, choice.mode);
440
- const explicit = Boolean(options.skill);
441
- const skills = explicit ? [await requireSkill(options)] : await bundledSkills();
531
+ const explicit = hasSelector(options);
532
+ const skills = explicit ? await requireSelection(options, "verify") : await bundledSkills();
442
533
  let failed = false;
443
534
  let found = 0;
444
535
  for (const skillName of skills) {
@@ -485,48 +576,74 @@ async function latestPublishedVersion() {
485
576
  }
486
577
  }
487
578
  async function removeSkill(options) {
488
- const skillName = await requireSkill(options);
579
+ const skillNames = await requireSelection(options, "remove");
489
580
  const choice = await prompt(options);
490
581
  const base = baseFor(choice.mode, options.dir);
491
582
  const agents = resolveAgents(choice.agent, choice.mode);
492
583
  if (!options.dryRun)
493
- return withInstallLock(base, () => removeLocked(options, skillName, agents, base));
494
- return removeLocked(options, skillName, agents, base);
495
- }
496
- async function removeLocked(options, skillName, agents, base) {
497
- let removed = 0;
498
- for (const agent of agents) {
499
- const destination = join(base, surfaces[agent], skillName);
500
- if (!await exists(destination)) {
501
- console.log(`not installed ${agent}: ${destination}`);
502
- continue;
503
- }
504
- await assertNoSymlink(destination, false);
505
- if (!options.force) {
506
- const { files } = await loadSource(skillName);
507
- const comparison = await compare(destination, files);
508
- if (comparison.status === "drifted")
509
- throw new Error(`Installation differs from the bundled skill (possibly locally modified); re-run with --force to remove anyway: ${destination}`);
510
- }
511
- if (options.dryRun) {
512
- console.log(`would remove ${agent}: ${destination}`);
513
- continue;
584
+ return withInstallLock(base, () => removeLocked(options, skillNames, agents, base));
585
+ return removeLocked(options, skillNames, agents, base, false);
586
+ }
587
+ async function removeLocked(options, skillNames, agents, base, recover = true) {
588
+ if (recover)
589
+ await recoverInstall(base);
590
+ const targets = [];
591
+ for (const skill of skillNames) {
592
+ for (const agent of agents) {
593
+ const destination = join(base, surfaces[agent], skill);
594
+ if (!await exists(destination)) {
595
+ console.log(`not installed ${agent}: ${destination}`);
596
+ continue;
597
+ }
598
+ await assertNoSymlink(destination, false);
599
+ if (!options.force) {
600
+ const { files } = await loadSource(skill);
601
+ const comparison = await compare(destination, files);
602
+ if (comparison.status === "drifted")
603
+ throw new Error(`Installation differs from the bundled skill (possibly locally modified); re-run with --force to remove anyway: ${destination}`);
604
+ }
605
+ targets.push({ skill, agent, destination });
514
606
  }
515
- await rm(destination, { recursive: true, force: true });
516
- removed += 1;
517
- console.log(`removed ${agent}: ${destination}`);
518
607
  }
519
- if (!removed && !options.dryRun)
608
+ if (options.dryRun) {
609
+ for (const target of targets)
610
+ console.log(`would remove ${target.agent}: ${target.destination}`);
611
+ return;
612
+ }
613
+ for (const target of targets) {
614
+ await rm(target.destination, { recursive: true, force: true });
615
+ console.log(`removed ${target.agent}: ${target.destination}`);
616
+ }
617
+ if (options.all) {
618
+ const skipped = (await skillCatalog()).filter((entry) => entry.repoOnly).map((entry) => entry.name);
619
+ if (skipped.length)
620
+ console.log(`left ${skipped.length} repo-only skills in place: ${skipped.join(", ")} — name one explicitly to remove it`);
621
+ }
622
+ if (!targets.length)
520
623
  process.exitCode = 1;
521
624
  }
522
625
  async function list() {
523
626
  const manifest = await loadManifest();
524
- for (const skillName of await bundledSkills()) {
525
- const skill = await readFile(join(bundleRoot, skillName, "SKILL.md"), "utf8");
627
+ const entries = await skillCatalog();
628
+ const groups = [...new Set(entries.map((entry) => entry.group).filter((group) => group !== null))].sort();
629
+ const show = async (entry) => {
630
+ const skill = await readFile(join(bundleRoot, entry.name, "SKILL.md"), "utf8");
526
631
  const description = skill.match(/^description: (.+)$/m)?.[1] ?? "";
527
- const fileCount = Object.keys(manifest.skills[skillName]?.files ?? {}).length;
528
- console.log(`${skillName} (${fileCount} files)`);
529
- console.log(` ${description.length > 160 ? `${description.slice(0, 157)}...` : description}`);
632
+ const fileCount = Object.keys(manifest.skills[entry.name]?.files ?? {}).length;
633
+ console.log(` ${entry.name} (${fileCount} files)${entry.repoOnly ? " [repo-only: not installed by --all]" : ""}`);
634
+ console.log(` ${description.length > 160 ? `${description.slice(0, 157)}...` : description}`);
635
+ };
636
+ for (const group of groups) {
637
+ console.log(`${group} — vegastack-skills add --group ${group}`);
638
+ for (const entry of entries.filter((item) => item.group === group).sort((a, b) => a.name.localeCompare(b.name)))
639
+ await show(entry);
640
+ console.log("");
641
+ }
642
+ const ungrouped = entries.filter((entry) => entry.group === null).sort((a, b) => a.name.localeCompare(b.name));
643
+ if (ungrouped.length) {
644
+ console.log("ungrouped");
645
+ for (const entry of ungrouped)
646
+ await show(entry);
530
647
  }
531
648
  }
532
649
  async function doctor(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegastack/skills",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Installer and verifier for VegaStack Agent Skills (Claude Code, Codex, and Hermes)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,7 +14,9 @@ honestly. VegaStack is a 3-4 person team; every extra service is maintenance som
14
14
  Nearest neighbors: `dev-setup` writes `.vegastack/dev.md` including its `## Architecture`
15
15
  section — setup owns the file, this skill owns the judgment reading it. `dev-intake` routes
16
16
  stack-bearing approach choices here while writing a brief; `vegastack-design-system` owns
17
- component and token choices inside the UI.
17
+ component and token choices inside the UI. Artifact formats — the register line included — follow
18
+ `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a
19
+ copy of.
18
20
 
19
21
  ## Every task
20
22
 
@@ -32,8 +34,8 @@ component and token choices inside the UI.
32
34
  3. Load only the references the task touches (table below). Do not bulk-read the set.
33
35
  4. Separate what is fact, what is assumption, and what is MK's recorded decision. A
34
36
  directive tagged "(inferred)" is a researched extrapolation MK has not ratified —
35
- confirm on first use, and a confirmation is proposed as a register line
36
- (`- DD-MM-YYYY operator (<username>) ratified: <the directive>`); recording it drops
37
+ confirm on first use, and a confirmation is proposed as a register line in conventions'
38
+ Operator identity format, its decision text `ratified: <the directive>`; recording it drops
37
39
  the tag from the reference file in the same change, so inferred never lingers as
38
40
  ratified-in-practice. Everything untagged is his recorded decision or a verified fact.
39
41
  Never re-litigate a recorded decision to route around a blocker — surface the blocker.
@@ -7,7 +7,7 @@ description: The project's narrative record — what got built, why, and how it
7
7
 
8
8
  `.vegastack/chronicle.md` is the project's story, newest first — the answer to "what did I build here and what happened?" months later, when the operator remembers nothing. Entries are **story language for a human**, never commit-log prose: the changelog tells consumers what changed; the chronicle tells the operator what happened.
9
9
 
10
- Nearest neighbors: `dev-implement` writes the entries at hand-back (the write rule lives there; the format lives here); `dev-status` answers "what needs me now" — this skill answers "how did we get here". `dev-ship`'s ship-gate checks entry presence when dev.md says `chronicle: on`.
10
+ Nearest neighbors: `dev-implement` writes the entries at hand-back (the write rule lives there; the format lives here); `dev-status` answers "what needs me now" — this skill answers "how did we get here". `dev-ship`'s ship-gate checks entry presence when dev.md says `chronicle: on`. Artifact formats follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
11
11
 
12
12
  ## The entry — one per behavior-changing branch
13
13
 
@@ -7,7 +7,7 @@ description: Reproduce-first bug work. Use when given a bug to fix — "debug th
7
7
 
8
8
  The failure this skill prevents: reading code, forming one theory, and "fixing" something that was never the cause. The discipline is a hard order — **reproduce, shrink, suspect, test, prove, clean** — and each phase has a completion criterion you can check, not vibe. It runs inside dev-implement's dark mode: no operator questions; missing-artifact stops are one `handback` comment; every phase result is a ledger checkpoint.
9
9
 
10
- Nearest neighbors: `dev-intake`'s bug variant writes the brief this skill executes; `dev-implement` owns the surrounding build ceremony; `dev-review` judges the finished fix.
10
+ Nearest neighbors: `dev-intake`'s bug variant writes the brief this skill executes; `dev-implement` owns the surrounding build ceremony; `dev-review` judges the finished fix. Artifact formats follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
11
11
 
12
12
  ## Phase 1 — the red command. No red command, no theorizing.
13
13
 
@@ -5,7 +5,7 @@ description: Implement an approved GitHub issue end to end without further user
5
5
 
6
6
  # dev-implement
7
7
 
8
- One issue, one session, end to end: preflight → claim → build dark → verify → review → evidence → stop. The operator reads the result in the issue on their own time; nothing here creates a PR or merges — those are `dev-ship`, on the operator's word. Artifact formats follow the `dev-setup` skill's `references/conventions.md`; the ledger discipline lives in [ledger-and-resume](references/ledger-and-resume.md).
8
+ One issue, one session, end to end: preflight → claim → build dark → verify → review → evidence → stop. The operator reads the result in the issue on their own time; nothing here creates a PR or merges — those are `dev-ship`, on the operator's word. Artifact formats follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of; the ledger discipline lives in [ledger-and-resume](references/ledger-and-resume.md).
9
9
 
10
10
  Nearest neighbors: `dev-plan` writes the plan this skill executes task by task; `dev-review` judges the result; issues that turn out to need decisions go back through `needs-operator`, never guessed. `.vegastack/dev.md` missing → run `dev-setup` first. Read dev.md before anything; its knobs govern this skill, and the `## Architecture` section governs stack-touching choices.
11
11
 
@@ -1,6 +1,6 @@
1
1
  # Using the ledger
2
2
 
3
- The ledger comment's format, line vocabulary, and the resume read-order (brief → plan → ledger → `git log`, nothing else) live in the `dev-setup` skill's `references/conventions.md` — this file owns only how dev-implement **uses** them. The ledger is the build's recovery map and the operator's live progress view.
3
+ The ledger comment's format, line vocabulary, and the resume read-order (brief → plan → ledger → `git log`, nothing else) live in `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of — this file owns only how dev-implement **uses** them. The ledger is the build's recovery map and the operator's live progress view.
4
4
 
5
5
  ## When to checkpoint
6
6
 
@@ -5,7 +5,7 @@ description: Turn ideas, brainstorms, feature requests, bug reports, or SOW docu
5
5
 
6
6
  # dev-intake
7
7
 
8
- Requirements come in as the operator's brainstorm, feature thought, bug report, or SOW; issues go out complete enough that a fresh agent needs nothing but the URL. Every question gets asked **here** — once implementation starts, dark mode means no questions, so an under-specified issue becomes either an interruption or a guess. Artifact formats (markers, operator identity, labels, revisions) follow the `dev-setup` skill's `references/conventions.md`.
8
+ Requirements come in as the operator's brainstorm, feature thought, bug report, or SOW; issues go out complete enough that a fresh agent needs nothing but the URL. Every question gets asked **here** — once implementation starts, dark mode means no questions, so an under-specified issue becomes either an interruption or a guess. Artifact formats (markers, operator identity, labels, revisions) follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
9
9
 
10
10
  Nearest neighbors: `dev-plan` owns the how once a brief is approved — intake owns the what/why and the approval mechanics; `dev-implement` builds. If `.vegastack/dev.md` is missing → run `dev-setup` first, then continue.
11
11
 
@@ -5,7 +5,7 @@ description: Write the implementation plan for an approved issue before any code
5
5
 
6
6
  # dev-plan
7
7
 
8
- The planning stage: an approved brief goes in, an operator-approved plan comes out, and only then does code exist. Plans are written against the repo **as it is now** — a separate session from intake for `full-plan` issues, because code drifts between brief approval and build. Formats follow the `dev-setup` skill's `references/conventions.md` (cited by name read it wherever dev-setup is installed).
8
+ The planning stage: an approved brief goes in, an operator-approved plan comes out, and only then does code exist. Plans are written against the repo **as it is now** — a separate session from intake for `full-plan` issues, because code drifts between brief approval and build. Formats follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
9
9
 
10
10
  Nearest neighbors: `dev-intake` owns the brief and its approval mechanics — this skill owns the how; `dev-implement` executes what this produces, task by task. If `.vegastack/dev.md` is missing → run `dev-setup` first.
11
11
 
@@ -37,7 +37,7 @@ Checkboxes belong to the implement session — never pre-tick them.
37
37
 
38
38
  ## Labels and approval
39
39
 
40
- Post the plan → flip to `needs-operator`. On the operator's "plan approved": record `<!-- vsk:v1 type=approval scope=plan -->` with their quoted words (`operator (<username>)` format), flip to `ready`, stop — building is dev-implement's.
40
+ Post the plan → flip to `needs-operator`. On the operator's "plan approved": record the approval marker comment per conventions with `scope=plan`, quoting their words in its operator-identity format, flip to `ready`, stop — building is dev-implement's.
41
41
 
42
42
  ## The ratchet — one home, this file
43
43
 
@@ -5,7 +5,7 @@ description: Independent review of finished implementation work — a diff again
5
5
 
6
6
  # dev-review
7
7
 
8
- Review is a specified system, not a vibe: fresh eyes per axis, severities with teeth, a bounded fix loop, and every dismissal on the record. The reviewer's job is findings or verified absence of findings — never praise. Formats follow the `dev-setup` skill's `references/conventions.md`; the reviewer briefs live in [dispatch-prompts](references/dispatch-prompts.md).
8
+ Review is a specified system, not a vibe: fresh eyes per axis, severities with teeth, a bounded fix loop, and every dismissal on the record. The reviewer's job is findings or verified absence of findings — never praise. Formats follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of; the reviewer briefs live in [dispatch-prompts](references/dispatch-prompts.md).
9
9
 
10
10
  Nearest neighbors: `dev-implement` invokes this per dev.md's `review:` knob and applies the findings; `dev-ship` consumes the verdict marker; `dev-plan`'s approval gate reviews plans before build — this skill reviews built work after.
11
11
 
@@ -65,7 +65,7 @@ Everything else — merge style, branch naming, the stop-and-ask list — takes
65
65
  | `AGENTS.md` | create it, or insert/replace only the block between `<!-- vsk-dev:start -->` and `<!-- vsk-dev:end -->` using the [agents-section template](assets/agents-section.md.template); content outside the markers is the user's and stays untouched |
66
66
  | `CLAUDE.md` | ensure its first line is `@AGENTS.md` — Claude Code does not read AGENTS.md natively and needs this import ([harness-facts](references/harness-facts.md)); create the file when absent |
67
67
  | labels | `gh label create <name> --color <hex> --description "<text>"` for the names the `labels:` knob records, skipping ones that exist; default names and creation colors ([conventions](references/conventions.md) holds meanings): state `needs-operator` FBCA04 · `needs-plan` E36209 · `ready` 0E8A16 · `working` 1D76DB · `for-operator` 5319E7; modifiers `risky` B60205 · scope `research` C5DEF5 · `quick-build` 76C7C0 · `full-plan` 2A9D8F · `epic` 24292E (only when the org has no native Epic issue type) |
68
- | decision register | create the file the `decisions:` knob names (default `.vegastack/decisions.md`) when missing, with a two-line header stating the format: `- DD-MM-YYYY operator (github-username) the decision` (username via `gh api user -q .login`, fallback `git config user.name`); a project with an existing register keeps it and the knob points there |
68
+ | decision register | create the file the `decisions:` knob names (default `.vegastack/decisions.md`) when missing, with a two-line header stating the register-line format conventions' Operator identity section defines (username via `gh api user -q .login`, fallback `git config user.name`); a project with an existing register keeps it and the knob points there |
69
69
  | guard workflows / hook files | only the ones the user said yes to in Round C |
70
70
 
71
71
  ## Step 4 — Report
@@ -7,7 +7,7 @@ description: Land finished work, each step only on the operator's explicit word.
7
7
 
8
8
  Gates spent only by the user's words: their words asking for a PR authorize the PR and nothing more; their words asking to merge authorize the merge. The dev.md `gates` knob sets how many actions one word covers — `3` keeps PR and merge as separate words, `2` lets one "ship it" cover both, `1` is direct-to-main (the ship word merges locally and pushes; no PR object, everything else identical). Passing checks, PR permissions, and the calendar authorize nothing by themselves.
9
9
 
10
- Nearest neighbor: `dev-implement` produces the `for-operator` issue with its evidence comment; ship packages and lands it. Corrections found here go back through implement's corrections loop.
10
+ Nearest neighbor: `dev-implement` produces the `for-operator` issue with its evidence comment; ship packages and lands it. Corrections found here go back through implement's corrections loop. Artifact formats — the register line included — follow `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
11
11
 
12
12
  ## Gate 1 — the PR
13
13
 
@@ -33,7 +33,7 @@ With `gates: 1` there is no PR: the same verifications run, then the ship word t
33
33
  On the user's merge instruction:
34
34
 
35
35
  - Re-check that the PR head is still the revision the evidence comment names and checks are green — a branch that moved since review gets re-verified before it lands.
36
- - Pending `Decision:` lines exist (issue comments, or the evidence comment's `**Decision:**` line) → name them in the merge confirmation — "merging will record: …" — so the operator's word demonstrably covers them; never append on inferred consent. On the word, append each to the register dev.md names (`decisions:` knob) in its `- DD-MM-YYYY operator (github-username) — …` format; the register is append-only and this is its moment.
36
+ - Pending `Decision:` lines exist (issue comments, or the evidence comment's `**Decision:**` line) → name them in the merge confirmation — "merging will record: …" — so the operator's word demonstrably covers them; never append on inferred consent. On the word, append each to the register dev.md names (`decisions:` knob) in conventions' Operator identity format; the register is append-only and this is its moment.
37
37
  - A merge conflict with the default branch is corrections work: update the branch, re-verify what the update touched, and the standing merge instruction holds once checks are green again — unless the update changed behavior, or more than 7 days have passed since the word; either way, re-confirm with one sentence rather than acting on a stale instruction.
38
38
  - Merge per the dev.md `merge` knob (default `gh pr merge --squash`; `gates: 1` merges locally per the same knob and pushes). `Closes #<n>` closes the issue; confirm both happened.
39
39
  - A bot PR (Renovate, Dependabot) has no issue or evidence comment and merging it is still shipping: green checks qualify it, only the operator's explicit word — per PR or per named batch — merges it; majors and security advisories get named before their word is acted on.
@@ -7,7 +7,7 @@ description: The operator's board — whose move is it, across every issue in th
7
7
 
8
8
  One question, answered from deterministic data: **whose move is it?** The bundled script gathers; this skill orders and narrates — it never invents state, and an unverifiable board is reported as exactly that.
9
9
 
10
- Nearest neighbors: `dev-chronicle` answers "how did we get here"; this skill answers "what needs whom right now". Formats read via the `dev-setup` skill's `references/conventions.md` markers.
10
+ Nearest neighbors: `dev-chronicle` answers "how did we get here"; this skill answers "what needs whom right now". Formats read via the markers in `references/conventions.md`, the spec `dev-setup` authors and every dev-family skill ships a copy of.
11
11
 
12
12
  ## Gather
13
13
 
@@ -9,14 +9,14 @@ Maintenance skill for this repository. Every change under `skills/` must satisfy
9
9
 
10
10
  ## Operating rules
11
11
 
12
- 1. Skill content lives only in `skills/<name>/`. Wiring lives outside it: the per-skill packaging allowlist in `packages/cli/packaging.json`, the root `README.md` skills table, and `CHANGELOG.md` (via changesets). The skillify scaffolder writes all three when creating a skill.
12
+ 1. Skill content lives only in the skill's own directory, at one of exactly two depths: `skills/<name>/` ungrouped, or `skills/<group>/<name>/` inside a group. Both are first-class; deeper is a build error. A group is a directory holding skills plus a `GROUP.md` (an H1 display title, then one blurb line), and group and skill names share one namespace and one grammar. Wiring lives outside the skill: the per-skill packaging allowlist in `packages/cli/packaging.json` — always keyed by **bare** skill name, because the packaged bundle is flat and an install command never carries a group — the root `README.md` skills table, and `CHANGELOG.md` (via changesets). The skillify scaffolder writes all three when creating a skill.
13
13
  2. Frontmatter is exactly two keys: `name` and `description`. The open spec also allows `license`, `compatibility`, and `metadata`, but this repo defaults to the minimal two; adding any other key is a policy exception needing maintainer sign-off.
14
14
  3. `name` must equal the directory name. Grammar: starts with a lowercase letter, then only lowercase letters, digits, and hyphens; no leading/trailing hyphen, no consecutive hyphens; at most 64 chars.
15
15
  4. `description`: at most 1024 chars, third person, states WHAT the skill does and WHEN to load it, trigger words front-loaded, no angle brackets. Never summarize the workflow in it — agents follow the summary and skip the body. Err slightly pushy: agents under-trigger.
16
16
  5. `SKILL.md` under 500 lines (target under 150) and roughly under 5k tokens. Detail goes to `references/`, executables to `scripts/`, templates to `assets/`. Relative links stay inside the skill, one level deep.
17
17
  6. No Claude-only body syntax anywhere in a skill: no dynamic command injection, no argument placeholders, no Claude environment-variable paths — the exact token list is in [standards](references/standards.md). Reference scripts as plain relative paths runnable from the skill directory.
18
18
  7. Never hand-edit checksums, versions, or timestamps in any `refresh/sources.json` — they must come from a refresh-runner run.
19
- 8. Before finishing any change: `node packages/cli/scripts/validate-skill.mjs skills/<name>` and `bun test skills/<name>` (both from repo root) must pass.
19
+ 8. Before finishing any change: `node packages/cli/scripts/validate-skill.mjs <skill-dir>` and `bun test <skill-dir>` for the skill you touched, plus `node packages/cli/scripts/structure.mjs check` for the repo shape (all from repo root) must pass. `bun run check` runs the structure check as one of its stages.
20
20
 
21
21
  ## Route progressively
22
22
 
@@ -24,13 +24,26 @@ Maintenance skill for this repository. Every change under `skills/` must satisfy
24
24
  |---|---|
25
25
  | tri-harness standards: discovery paths, frontmatter rules, context budgets, install surfaces, portability rules, unverified items | [standards](references/standards.md) |
26
26
  | new skill: should-it-exist gate, scaffolding, the 8-item contract audit, behavioral evals | the `skillify` skill |
27
+ | repo shape: groups, `GROUP.md`, the README sections, the structure check | the group workflow below |
27
28
  | release, rename, deprecate, rollback mechanics | [release ops](references/release-ops.md) |
28
29
  | this skill's own freshness contract | [REFRESH](refresh/REFRESH.md), [sources](refresh/sources.json) |
29
30
  | authoritative repo policy | `CONTRIBUTING.md` and `.vegastack/dev.md` at the repo root (release runbook, content semver, rollback) |
30
31
 
32
+ ## Workflow: create or maintain a group
33
+
34
+ Groups are this skill's responsibility: the repo shape, its `GROUP.md` files, and the root README sections that mirror them. The deterministic method is `packages/cli/scripts/structure.mjs`, run from the repo root.
35
+
36
+ 1. **Create a group** — `node packages/cli/scripts/structure.mjs create-group <name> --title "<Display Title>" --blurb "<one line>"` prints the plan; add `--write` to apply. It writes `skills/<name>/GROUP.md` and inserts the matching `### <Display Title>` README section after the existing tables. Dry-run by default, and idempotent when re-run with the same title. Every refusal happens before it writes anything: a name that breaks the grammar, names an existing skill at either depth, or resolves to a symlink; a title another group already uses, or a different title on an existing group (that is a rename — edit `GROUP.md` and the README heading together); a title or blurb that would produce a `GROUP.md` its own reader rejects; an existing but malformed `GROUP.md`; and a README with no `## Skills` table to hold the section.
37
+ 2. **Put skills in it** — skillify's scaffolder places a new skill with `--group <name>`; it refuses — before writing anything — an unknown group, a malformed `GROUP.md`, a group with no README section, and a skill name already used at either depth, so a mistyped group never creates a stray family and the flat bundle never gets two skills of one name. Moving an existing skill into a group is a `git mv` plus its README row and its test's validator-import depth; `structure.mjs check` names anything left inconsistent.
38
+ 3. **Check the shape** — `node packages/cli/scripts/structure.mjs check` blocks on illegal depth, name collisions, a missing or malformed `GROUP.md`, two groups sharing one `GROUP.md` title, a group with no README section, stray files in a group, a skill missing a contract meta file, packaging entries that disagree with the authored tree, a group-qualified packaging key, and README rows that are absent, duplicated, mispathed, or in the wrong section. It warns — without blocking, so warnings never fail `bun run check` — on an empty group, a group of one, and scaffolded placeholder text; `--strict` makes warnings exit 1. Dot-prefixed files and directories under `skills/` are ignored everywhere — discovery, this check, and the packaging build alike: they are tool and OS leftovers, not skills.
39
+
40
+ A group never reaches an installed **path**: the bundle is flat, so `GROUP.md` ships nowhere and an installed skill is always `<surface>/<bare-name>/`. A group is a *selection*, and the installer does expose it — `add`, `verify`, and `remove` each take one of a skill name, `--group <name>`, or `--all`, and a group install is one all-or-nothing transaction. Ungrouped skills at `skills/<name>/` stay fully supported; grouping is a choice, not a migration.
41
+
42
+ Two skills are marked repo-only in `packages/cli/repo-only.json`: `skill-maintainer` and `skillify` operate on this repository and do nothing useful elsewhere, so `--all` skips them while naming one explicitly still installs it. That list is data, validated by the build against the authored skills — never inferred from a skill's prose. Repo-only and group membership are independent: a group says where a skill is authored, the marker says who should install it.
43
+
31
44
  ## Workflow: scaffold a new skill
32
45
 
33
- 1. Run skillify's `scripts/scaffold-skill.mjs` — it creates the contract tree (`SKILL.md`, `README.md`, `refresh/`, `agents/openai.yaml`, tests and the trigger fixture) and performs the repo wiring itself: the `packages/cli/packaging.json` entry, the root README row, and the changeset (a new skill is MINOR). Fill in the README row description and changeset text it leaves as TODOs; files added after scaffolding go into the skill's `packaging.json` entry by hand (the build fails loudly on unlisted files).
46
+ 1. Run skillify's `scripts/scaffold-skill.mjs` — it creates the contract tree (`SKILL.md`, `README.md`, `refresh/`, `agents/openai.yaml`, tests and the trigger fixture) and performs the repo wiring itself: the `packages/cli/packaging.json` entry, the root README row in the right section, and the changeset (a new skill is MINOR). Pass `--group <name>` to place it in an existing group. Fill in the README row description and changeset text it leaves as placeholders; files added after scaffolding go into the skill's `packaging.json` entry by hand (the build fails loudly on unlisted files).
34
47
  2. Write frontmatter and body per operating rules 2–6; the body routes to references, it does not inline them. `skillify` owns the full authoring and eval discipline.
35
48
  3. Seed refresh baselines with the deterministic runner, never by hand — invocation in [REFRESH](refresh/REFRESH.md).
36
49
  4. Run the checks in operating rule 8.
@@ -61,7 +74,7 @@ Run this checklist before merging any skill change; per-harness detail in [stand
61
74
  - [ ] No Claude-only body syntax (token list in standards).
62
75
  - [ ] `agents/openai.yaml` present for Codex; extra files are safely ignored by the other harnesses.
63
76
  - [ ] Install surfaces respected: Claude Code `.claude/skills` (project) / `~/.claude/skills` (global); Codex `.agents/skills` (project and global); Hermes `~/.hermes/skills` **global only** — never assume Hermes project-level discovery.
64
- - [ ] `node packages/cli/scripts/validate-skill.mjs skills/<name>` and `bun test skills/<name>` pass.
77
+ - [ ] `node packages/cli/scripts/validate-skill.mjs <skill-dir>` and `bun test <skill-dir>` pass, and `node packages/cli/scripts/structure.mjs check` is clean.
65
78
 
66
79
  ## Hard limits
67
80
 
@@ -46,14 +46,14 @@ Skill names are consumer-facing identifiers — treat a rename as a stable-ID br
46
46
  2. Update every wiring point in the same PR: the skill's entry in `packages/cli/packaging.json`, the root README skills table row, and any cross-skill or docs links.
47
47
  3. Changeset: MINOR by default — major only when the operator declares it. Either way, note the migration in `CHANGELOG.md`: copies installed under the old name are orphaned, and installer operations addressed to the old name stop resolving once the shipped manifest no longer knows it.
48
48
  4. Registry source IDs inside `refresh/sources.json` are skill-internal and unaffected, but every `affected` ref must still resolve to a real reference in the renamed tree.
49
- 5. Re-run `node packages/cli/scripts/validate-skill.mjs skills/<new-name>` and the skill's tests — name/directory equality is validated.
49
+ 5. Re-run `node packages/cli/scripts/validate-skill.mjs <new-skill-dir>`, the skill's tests, and `node packages/cli/scripts/structure.mjs check` — name/directory equality is validated, and the structure check catches a README row left pointing at the old path.
50
50
 
51
51
  ## Deprecate / remove a skill
52
52
 
53
53
  1. Announce deprecation in the root README table and `CHANGELOG.md` at least one release before removal when practical.
54
- 2. Removal: delete `skills/<name>/`, remove its allowlist entries and README row, MAJOR changeset with migration notes.
54
+ 2. Removal: delete the skill's directory, remove its allowlist entries and README row, MAJOR changeset with migration notes. Removing the last skill from a group also removes the group's `GROUP.md` and its README section.
55
55
  3. Removing a skill in a new MAJOR does **not** deprecate previously published package versions — `npm deprecate` only versions that are themselves broken.
56
56
 
57
57
  ## Refresh branches
58
58
 
59
- Branches named `refresh/**` are reserved for the automated freshness loop and are CI-restricted to `skills/*/refresh/`. Human content changes go on normal branches. Never hand-edit checksums/versions/timestamps anywhere — CI re-fetches claimed baselines, so hand-edited values cannot merge.
59
+ Branches named `refresh/**` are reserved for the automated freshness loop and are CI-restricted to refresh metadata at either legal depth (`skills/<name>/refresh/` and `skills/<group>/<name>/refresh/`). Human content changes go on normal branches. Never hand-edit checksums/versions/timestamps anywhere — CI re-fetches claimed baselines, so hand-edited values cannot merge.
@@ -68,6 +68,8 @@ Source: https://hermes-agent.nousresearch.com/docs/user-guide/features/skills. <
68
68
 
69
69
  Installer implication: the `@vegastack/skills` installer must treat Hermes as global-only — a "project install" for Hermes does not exist.
70
70
 
71
+ These paths are the harnesses' own discovery rules and are unaffected by how skills are selected. The installer's `--group` and `--all` flags choose *which* skills to act on; an installed skill is always `<surface>/<bare-name>/`, never `<surface>/<group>/<name>/`.
72
+
71
73
  ## Portability rules (this repo's policy)
72
74
 
73
75
  One authored tree, three harnesses. Every skill in `skills/` follows all seven:
@@ -13,10 +13,10 @@ Instructions for the scheduled refresh agent (and any human running a manual ref
13
13
  The deterministic runner is repo-shared, hosted at `tooling/refresh/`. Run from the repo root:
14
14
 
15
15
  1. **Deterministic pass first** (no LLM judgment):
16
- `node tooling/refresh/refresh-evidence.mjs --registry skills/skill-maintainer/refresh/sources.json`
16
+ `node tooling/refresh/refresh-evidence.mjs --registry skills/repo-tooling/skill-maintainer/refresh/sources.json`
17
17
  drift/stale/unavailable results are the work-list. Exit 1 with a critical entry means fail-closed: the run must not be silently skipped. All four sources here are critical.
18
18
  2. **Accept verified changes** in the same code path:
19
- `node tooling/refresh/refresh-evidence.mjs --registry skills/skill-maintainer/refresh/sources.json --accept-baselines`
19
+ `node tooling/refresh/refresh-evidence.mjs --registry skills/repo-tooling/skill-maintainer/refresh/sources.json --accept-baselines`
20
20
  This writes registry, cache, and drift report together — never hand-edit checksums, versions, or timestamps; they must always come from a run. Baselines are runner-seeded; when a new source is added or a verified change is accepted, this accept-baselines invocation is the only sanctioned way to update them.
21
21
  3. **Semantic verification** for every source the deterministic pass flagged: read the changed page (fetch the registry URL), decide whether any `<!-- source: X -->` marked sentence in `references/standards.md` (or the mirrored SKILL.md hard-limits row) is now wrong, and propose the minimal edit. Unlike version-pin registries, checksum drift here is presumed meaningful until a human reads the diff — these pages define the standards themselves. Editorial churn may be accepted silently only after that read.
22
22
  4. **One standing refresh PR**, branch `refresh/weekly`, force-updated on every run (never stacked duplicates; the weekly workflow .github/workflows/refresh.yml maintains it). PR body lists: each changed source, old→new checksum, links to the evidence, and which marked sentences changed and why. A maintainer review is mandatory before merge.
@@ -5,7 +5,7 @@ description: Repo-local skill factory and auditor for the vegastack-skills monor
5
5
 
6
6
  # Skillify
7
7
 
8
- Turn a raw workflow into a properly-skilled unit of this monorepo, or audit an existing `skills/<name>/` tree against the repo contract (CONTRIBUTING.md, "Adding a new skill"). Skillify is repo-only: it creates and scores skills in this repository, not anywhere else. Deep wiring/release mechanics belong to `skill-maintainer`; cross-reference it rather than restating it.
8
+ Turn a raw workflow into a properly-skilled unit of this monorepo, or audit an existing skill tree — `skills/<name>/` or `skills/<group>/<name>/` — against the repo contract (CONTRIBUTING.md, "Adding a new skill"). Skillify is repo-only: it creates and scores skills in this repository, not anywhere else. Deep wiring/release mechanics belong to `skill-maintainer`; cross-reference it rather than restating it.
9
9
 
10
10
  ## The contract checklist
11
11
 
@@ -41,7 +41,7 @@ Scope upper bound: **one skill = one capability = one coherent trigger family.**
41
41
  For an existing skill, score the checklist against the actual tree and stop with the verdict:
42
42
 
43
43
  ```
44
- Skill: <name> Path: skills/<name>/
44
+ Skill: <name> Path: skills/<name>/ | skills/<group>/<name>/
45
45
  Score: <passed>/8 Verdict: <verdict>
46
46
  Missing: <item>: <one-line evidence> ...
47
47
  ```
@@ -94,8 +94,9 @@ Now that quality is proven:
94
94
  ## Phase 6 — Verify
95
95
 
96
96
  ```sh
97
- node packages/cli/scripts/validate-skill.mjs skills/<name>
98
- bun test skills/<name>
97
+ node packages/cli/scripts/validate-skill.mjs <skill-dir>
98
+ bun test <skill-dir>
99
+ node packages/cli/scripts/structure.mjs check
99
100
  bun run check
100
101
  ```
101
102
 
@@ -5,7 +5,7 @@ TODO: one-paragraph summary for humans and agents browsing the repo. The agent e
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- npx @vegastack/skills add {{name}}
8
+ npx @vegastack/skills add {{name}}{{groupInstallLine}}
9
9
  ```
10
10
 
11
11
  ## What's in this skill
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { join, resolve } from 'node:path'
4
- import { validateSkill } from '../../../packages/cli/scripts/validate-skill.mjs'
4
+ import { validateSkill } from '{{validatorPath}}'
5
5
 
6
6
  const skillRoot = resolve(import.meta.dir, '..')
7
7
 
@@ -9,7 +9,7 @@ Instructions for the scheduled refresh agent (and any human running a manual ref
9
9
  - the numeric limits and harness listing budgets table in `references/authoring.md` (marked `<!-- mirrored -->`);
10
10
  - the model guidance section in `references/eval-playbook.md` (marked `<!-- volatile -->`).
11
11
 
12
- Both mirror facts whose sources of truth are the agent-skills standards sources tracked in the **skill-maintainer** registry (`skills/skill-maintainer/refresh/sources.json`). `sources` here is therefore deliberately empty, and the refresh runner treats this registry as valid with nothing to fetch.
12
+ Both mirror facts whose sources of truth are the agent-skills standards sources tracked in the **skill-maintainer** registry (`skills/repo-tooling/skill-maintainer/refresh/sources.json`). `sources` here is therefore deliberately empty, and the refresh runner treats this registry as valid with nothing to fetch.
13
13
 
14
14
  ## How to refresh
15
15
 
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Scaffold a new skill tree at skills/<name>/ from skillify's templates.
2
+ // Scaffold a new skill tree at skills/<name>/ (or skills/<group>/<name>/) from skillify's
3
+ // templates.
3
4
  //
4
- // node scripts/scaffold-skill.mjs <skill-name> --dir <repo-root> [--write] [--json]
5
+ // node scripts/scaffold-skill.mjs <skill-name> --dir <repo-root> [--group <group>] [--write] [--json]
6
+ //
7
+ // --group places the skill in an existing group. Creating a group is skill-maintainer's job
8
+ // (packages/cli/scripts/structure.mjs create-group), so an unknown group is refused rather than
9
+ // invented: a mistyped group must never bring a stray family into existence.
5
10
  //
6
11
  // Dry-run by default: prints the plan (files that would be created plus the
7
12
  // wiring actions that would be performed) and creates nothing. --write stages
@@ -9,14 +14,14 @@
9
14
  // refusing existing directories and symlinks, then performs the repo wiring
10
15
  // itself: packaging.json entry, root README row, changeset. Exit codes: 0 ok,
11
16
  // 1 refusal or failure, 2 usage error.
12
- import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
17
+ import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
13
18
  import { dirname, join, resolve } from 'node:path'
14
19
  import { fileURLToPath } from 'node:url'
15
20
 
16
21
  const here = dirname(fileURLToPath(import.meta.url))
17
22
  const templatesRoot = resolve(here, '..', 'assets', 'templates')
18
23
 
19
- // Template file -> output path inside skills/<name>/ (null = tests/<name>.test.ts).
24
+ // Template file -> output path inside the skill (null = tests/<name>.test.ts).
20
25
  export const templateFiles = [
21
26
  ['SKILL.md.template', 'SKILL.md'],
22
27
  ['README.md.template', 'README.md'],
@@ -44,6 +49,31 @@ export function validateName(name) {
44
49
  return null
45
50
  }
46
51
 
52
+ // This script ships inside the skillify skill, so it must stay dependency-free and cannot import
53
+ // the repo's lib/skills.mjs. Deliberate small duplication of its GROUP.md title read; the two are
54
+ // kept honest by structure.mjs check, which fails when the README and GROUP.md disagree.
55
+ export function groupTitle(markdown) {
56
+ const lines = markdown.split('\n')
57
+ const headingIndex = lines.findIndex(line => /^#\s+\S/.test(line))
58
+ if (headingIndex === -1) return null
59
+ const title = lines[headingIndex].replace(/^#\s+/, '').trim()
60
+ const blurb = lines.slice(headingIndex + 1).find(line => line.trim() !== '')?.trim()
61
+ if (!title || !blurb || blurb.startsWith('#')) return null
62
+ return title
63
+ }
64
+
65
+ // Two-level scan mirroring lib/skills.mjs's discovery. Duplicated for the same reason as
66
+ // groupTitle: this script ships inside the skillify skill and cannot import repo tooling.
67
+ async function findSkillAnywhere(skillsRoot, name) {
68
+ if ((await entryAt(join(skillsRoot, name, 'SKILL.md')))?.isFile()) return join(skillsRoot, name)
69
+ for (const entry of await readdir(skillsRoot, { withFileTypes: true })) {
70
+ if (!entry.isDirectory()) continue
71
+ const path = join(skillsRoot, entry.name, name)
72
+ if ((await entryAt(join(path, 'SKILL.md')))?.isFile()) return path
73
+ }
74
+ return null
75
+ }
76
+
47
77
  async function entryAt(path) {
48
78
  try {
49
79
  return await lstat(path)
@@ -73,21 +103,50 @@ async function wirePackaging(repoRoot, name, write) {
73
103
  return { step: 'packaging.json entry', status: 'done' }
74
104
  }
75
105
 
76
- // Inserts a row for the new skill at the end of the root README's Skills table.
77
- async function wireReadme(repoRoot, name, write) {
106
+ // Resolves where a row belongs: the end of the ungrouped table, or of the table under the
107
+ // group's "### <title>" section. Both windows are bounded by the "## Skills" region, so a row can
108
+ // never land in a neighbouring family's table or in an unrelated table elsewhere in the README.
109
+ // Returns null when the README has no usable table, so callers can refuse before writing rather
110
+ // than report a "skipped:" success afterwards.
111
+ export function findRowInsertion(lines, group, groupHeading) {
112
+ const regionStart = lines.findIndex(line => /^##\s+Skills\s*$/.test(line))
113
+ if (regionStart < 0) return null
114
+ const afterRegion = lines.findIndex((line, index) => index > regionStart && /^##\s+/.test(line) && !/^###/.test(line))
115
+ const regionEnd = afterRegion < 0 ? lines.length : afterRegion
116
+
117
+ let from = regionStart
118
+ let to = regionEnd
119
+ if (group) {
120
+ from = lines.findIndex((line, index) => index > regionStart && index < regionEnd && line.trim() === `### ${groupHeading}`)
121
+ if (from < 0) return { missingSection: true }
122
+ const next = lines.findIndex((line, index) => index > from && index < regionEnd && /^###\s+/.test(line))
123
+ to = next < 0 ? regionEnd : next
124
+ } else {
125
+ const firstSection = lines.findIndex((line, index) => index > regionStart && index < regionEnd && /^###\s+/.test(line))
126
+ if (firstSection >= 0) to = firstSection
127
+ }
128
+
129
+ const header = lines.findIndex((line, index) => index >= from && index < to && /^\| *Skill *\|/.test(line))
130
+ if (header < 0 || !/^\|[ -]*---/.test(lines[header + 1] ?? '')) return null
131
+ let last = header + 1
132
+ while (last + 1 < to && lines[last + 1]?.startsWith('|')) last += 1
133
+ return { index: last }
134
+ }
135
+
136
+ async function wireReadme(repoRoot, name, group, groupHeading, write) {
78
137
  const path = join(repoRoot, 'README.md')
79
138
  if (!(await entryAt(path))?.isFile()) return { step: 'root README row', status: 'skipped: README.md not found' }
80
139
  const body = await readFile(path, 'utf8')
81
- if (body.includes(`](skills/${name}/)`)) return { step: 'root README row', status: 'skipped: row already exists' }
140
+ const relativePath = group ? `${group}/${name}` : name
141
+ if (body.includes(`](skills/${relativePath}/)`)) return { step: 'root README row', status: 'skipped: row already exists' }
82
142
  const lines = body.split('\n')
83
- const header = lines.findIndex(line => /^\| *Skill *\|/.test(line))
84
- if (header < 0 || !/^\|[ -]*---/.test(lines[header + 1] ?? '')) {
85
- return { step: 'root README row', status: 'skipped: Skills table not found' }
86
- }
87
- let last = header + 1
88
- while (lines[last + 1]?.startsWith('|')) last += 1
143
+
144
+ const target = findRowInsertion(lines, group, groupHeading)
145
+ if (target?.missingSection) throw new Error(`README.md has no "### ${groupHeading}" section for group "${group}" - create it with structure.mjs create-group`)
146
+ if (!target) return { step: 'root README row', status: 'skipped: Skills table not found' }
147
+ const last = target.index
89
148
  if (!write) return { step: 'root README row', status: 'planned' }
90
- const row = `| [${name}](skills/${name}/) | TODO: one-line description | [Walkthrough](skills/${name}/README.md) · [SKILL.md](skills/${name}/SKILL.md) |`
149
+ const row = `| [${name}](skills/${relativePath}/) | TODO: one-line description | [Walkthrough](skills/${relativePath}/README.md) · [SKILL.md](skills/${relativePath}/SKILL.md) |`
91
150
  lines.splice(last + 1, 0, row)
92
151
  await writeAtomic(path, lines.join('\n'))
93
152
  return { step: 'root README row', status: 'done' }
@@ -104,15 +163,24 @@ async function wireChangeset(repoRoot, name, write) {
104
163
  return { step: 'changeset', status: 'done' }
105
164
  }
106
165
 
107
- export async function wireSkill({ name, repoRoot, write = false }) {
166
+ export async function wireSkill({ name, repoRoot, group = null, groupHeading = null, write = false }) {
167
+ // groupHeading is an optimisation for scaffoldSkill, which has already read GROUP.md. A caller
168
+ // using the documented { name, repoRoot, group, write } shape gets it derived here rather than
169
+ // a row addressed to "### null".
170
+ if (group && !groupHeading) {
171
+ const doc = await entryAt(join(repoRoot, 'skills', group, 'GROUP.md'))
172
+ if (!doc?.isFile()) throw new Error(`Group "${group}" has no GROUP.md - every group carries one`)
173
+ groupHeading = groupTitle(await readFile(join(repoRoot, 'skills', group, 'GROUP.md'), 'utf8'))
174
+ if (!groupHeading) throw new Error(`skills/${group}/GROUP.md is malformed - it needs an H1 title followed by one non-empty blurb line`)
175
+ }
108
176
  return [
109
177
  await wirePackaging(repoRoot, name, write),
110
- await wireReadme(repoRoot, name, write),
178
+ await wireReadme(repoRoot, name, group, groupHeading, write),
111
179
  await wireChangeset(repoRoot, name, write),
112
180
  ]
113
181
  }
114
182
 
115
- export async function scaffoldSkill({ name, dir, write = false, now = new Date() }) {
183
+ export async function scaffoldSkill({ name, dir, group = null, write = false, now = new Date() }) {
116
184
  const nameError = validateName(name)
117
185
  if (nameError) throw new Error(`Invalid skill name ${JSON.stringify(name ?? null)}: ${nameError}`)
118
186
  if (!dir) throw new Error('--dir <repo-root> is required')
@@ -123,19 +191,74 @@ export async function scaffoldSkill({ name, dir, write = false, now = new Date()
123
191
  if (!skillsEntry || !skillsEntry.isDirectory()) {
124
192
  throw new Error(`${skillsRoot} is not a real directory - point --dir at the vegastack-skills repo root`)
125
193
  }
126
- const target = join(skillsRoot, name)
194
+
195
+ // A group must already exist and carry a well-formed GROUP.md; creating one is
196
+ // skill-maintainer's structure.mjs create-group, never a side effect of scaffolding a skill.
197
+ let groupHeading = null
198
+ if (group) {
199
+ const groupError = validateName(group)
200
+ if (groupError) throw new Error(`Invalid group name ${JSON.stringify(group)}: ${groupError}`)
201
+ const groupRoot = join(skillsRoot, group)
202
+ const groupEntry = await entryAt(groupRoot)
203
+ if (!groupEntry || !groupEntry.isDirectory()) {
204
+ throw new Error(`Group "${group}" does not exist at ${groupRoot} - create it first with: node packages/cli/scripts/structure.mjs create-group ${group} --title <title> --blurb <blurb> --write`)
205
+ }
206
+ const groupDoc = await entryAt(join(groupRoot, 'GROUP.md'))
207
+ if (!groupDoc?.isFile()) throw new Error(`Group "${group}" has no GROUP.md - every group carries one; see skill-maintainer's group workflow`)
208
+ groupHeading = groupTitle(await readFile(join(groupRoot, 'GROUP.md'), 'utf8'))
209
+ if (!groupHeading) throw new Error(`skills/${group}/GROUP.md is malformed - it needs an H1 title followed by one non-empty blurb line`)
210
+ }
211
+
212
+ const parent = group ? join(skillsRoot, group) : skillsRoot
213
+ const target = join(parent, name)
127
214
  if (await entryAt(target)) throw new Error(`Refusing to scaffold: ${target} already exists`)
128
215
 
216
+ // Skill names are unique across the whole tree, not just within one directory: the packaged
217
+ // bundle is flat, so a duplicate at the other depth would break the next build. Checked here,
218
+ // before anything is written, rather than left to that build.
219
+ const clash = await findSkillAnywhere(skillsRoot, name)
220
+ if (clash) throw new Error(`Refusing to scaffold: a skill named "${name}" already exists at ${clash} - the packaged bundle is flat, so skill names are unique across the whole tree`)
221
+
222
+ // Every refusal belongs in this pre-flight. wireReadme runs after the tree is renamed into
223
+ // place and the packaging entry written, so anything discovered there would leave a half-wired
224
+ // skill on disk while reporting a refusal - or, worse, report success with no row at all.
225
+ // When a README exists, its row target must be resolvable BEFORE the tree is staged, so a
226
+ // refusal writes nothing. A wholly absent README or packaging.json keeps the documented
227
+ // degrade-to-`skipped:` contract (skillify's README, and the bare-repo test) - tightening that
228
+ // is a behaviour change of its own, tracked separately.
229
+ const readmePath = join(repoRoot, 'README.md')
230
+ if ((await entryAt(readmePath))?.isFile()) {
231
+ const lines = (await readFile(readmePath, 'utf8')).split('\n')
232
+ const rowTarget = findRowInsertion(lines, group, groupHeading)
233
+ if (rowTarget?.missingSection) {
234
+ throw new Error(`README.md has no "### ${groupHeading}" section for group "${group}" - create it with structure.mjs create-group`)
235
+ }
236
+ if (!rowTarget) {
237
+ throw new Error(`README.md has no ${group ? `table under "### ${groupHeading}"` : 'ungrouped Skills table'} to add a row to - every skill needs its row, so refusing rather than scaffolding a skill the structure check would block`)
238
+ }
239
+ }
240
+
241
+ // The generated test imports the repo validator by relative path, so its depth follows the
242
+ // skill's: skills/<name>/tests/ is three levels up, skills/<group>/<name>/tests/ is four.
243
+ const validatorPath = `${group ? '../../../..' : '../../..'}/packages/cli/scripts/validate-skill.mjs`
244
+ // Only a grouped skill gets the family-install line; an ungrouped one would otherwise ship a
245
+ // command naming a group that does not exist.
246
+ const groupInstallLine = group ? `\nnpx @vegastack/skills add --group ${group} # or the whole ${group} family` : ''
247
+
129
248
  const outputs = templateFiles.map(([source, output]) => [source, output ?? `tests/${name}.test.ts`])
130
- const plan = { name, target, files: outputs.map(([, output]) => output), wrote: false }
131
- if (!write) return { ...plan, wiring: await wireSkill({ name, repoRoot }) }
249
+ const plan = { name, group, target, files: outputs.map(([, output]) => output), wrote: false }
250
+ if (!write) return { ...plan, wiring: await wireSkill({ name, repoRoot, group, groupHeading }) }
132
251
 
133
252
  const date = now.toISOString().slice(0, 10)
134
- const staging = await mkdtemp(join(skillsRoot, `.${name}.scaffold-`))
253
+ const staging = await mkdtemp(join(parent, `.${name}.scaffold-`))
135
254
  try {
136
255
  for (const [source, output] of outputs) {
137
256
  const body = await readFile(join(templatesRoot, source), 'utf8')
138
- const rendered = body.replaceAll('{{name}}', name).replaceAll('{{date}}', date)
257
+ const rendered = body
258
+ .replaceAll('{{name}}', name)
259
+ .replaceAll('{{date}}', date)
260
+ .replaceAll('{{validatorPath}}', validatorPath)
261
+ .replaceAll('{{groupInstallLine}}', groupInstallLine)
139
262
  const destination = join(staging, output)
140
263
  await mkdir(dirname(destination), { recursive: true })
141
264
  await writeFile(destination, rendered)
@@ -146,11 +269,11 @@ export async function scaffoldSkill({ name, dir, write = false, now = new Date()
146
269
  await rm(staging, { recursive: true, force: true })
147
270
  throw error
148
271
  }
149
- return { ...plan, wrote: true, wiring: await wireSkill({ name, repoRoot, write: true }) }
272
+ return { ...plan, wrote: true, wiring: await wireSkill({ name, repoRoot, group, groupHeading, write: true }) }
150
273
  }
151
274
 
152
275
  function parseArguments(argv) {
153
- const options = { name: undefined, dir: undefined, write: false, json: false }
276
+ const options = { name: undefined, dir: undefined, group: null, write: false, json: false }
154
277
  const rest = [...argv]
155
278
  while (rest.length) {
156
279
  const flag = rest.shift()
@@ -158,6 +281,10 @@ function parseArguments(argv) {
158
281
  const value = rest.shift()
159
282
  if (value === undefined || value.startsWith('-')) throw new Error('--dir requires a value')
160
283
  options.dir = value
284
+ } else if (flag === '--group') {
285
+ const value = rest.shift()
286
+ if (value === undefined || value.startsWith('-')) throw new Error('--group requires a value')
287
+ options.group = value
161
288
  } else if (flag === '--write') options.write = true
162
289
  else if (flag === '--json') options.json = true
163
290
  else if (flag.startsWith('-')) throw new Error(`Unknown option: ${flag}`)
@@ -165,7 +292,7 @@ function parseArguments(argv) {
165
292
  else throw new Error(`Unexpected argument: ${flag}`)
166
293
  }
167
294
  if (!options.name || !options.dir) {
168
- throw new Error('Usage: node scripts/scaffold-skill.mjs <skill-name> --dir <repo-root> [--write] [--json]')
295
+ throw new Error('Usage: node scripts/scaffold-skill.mjs <skill-name> --dir <repo-root> [--group <group>] [--write] [--json]')
169
296
  }
170
297
  return options
171
298
  }
@@ -2,8 +2,10 @@
2
2
  "schemaVersion": 2,
3
3
  "skills": {
4
4
  "dev-architect": {
5
+ "group": "dev-skills",
6
+ "repoOnly": false,
5
7
  "files": {
6
- "SKILL.md": "3b0b6f2d18f63f644bb396be9fdd7fbc8c8473c0b29190741c9623407bf82cc3",
8
+ "SKILL.md": "74150dde2d3c83598769a4c9f8cd295a7496d9eb9a5e920a29cba8464eb58128",
7
9
  "agents/openai.yaml": "99023b056d5c568ff3acc059e5bb7a4e6cfa5261cd0d35a525407f9e159de562",
8
10
  "references/ai-agents.md": "565e5d480f7e342adf43f9b864ab4cb66b7e513f0ea676de66909fe0ccd95988",
9
11
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
@@ -20,8 +22,10 @@
20
22
  }
21
23
  },
22
24
  "dev-chronicle": {
25
+ "group": "dev-skills",
26
+ "repoOnly": false,
23
27
  "files": {
24
- "SKILL.md": "e43f50fc570992de0766d4e569693babd05cde08d2e1c271c267df6b7552ac89",
28
+ "SKILL.md": "23c6266758726d6435de38cb0cc142a94a0b55f2329f7ab6404e42fa5d0dee3a",
25
29
  "agents/openai.yaml": "765b12e17b7fce88c53ca0fae735bf0efceb843cc3703b91bc1801ab5cac06b4",
26
30
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
27
31
  "refresh/REFRESH.md": "760e66642eec426cd5ab2319613e817adf5bbd7b64c8eaa22dd13497351bf9c1",
@@ -29,8 +33,10 @@
29
33
  }
30
34
  },
31
35
  "dev-debug": {
36
+ "group": "dev-skills",
37
+ "repoOnly": false,
32
38
  "files": {
33
- "SKILL.md": "62f86b8b88a66c6e8f44eba434fdc8b8dc044deda814cb9a2d1e8ec0dee79538",
39
+ "SKILL.md": "38ba9e67c00925f4e4738be333dc398218008df929fcf17b61398966ce4deee1",
34
40
  "agents/openai.yaml": "4aa52b5c620b0bf4de7f1609867956ce762d83dc4297ab5670a68c206ae63075",
35
41
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
36
42
  "references/loop-ladder.md": "ddcb45ec2241bc5dc25936348074cdcc8f9af848e90b363351a7faf97007c256",
@@ -39,11 +45,13 @@
39
45
  }
40
46
  },
41
47
  "dev-implement": {
48
+ "group": "dev-skills",
49
+ "repoOnly": false,
42
50
  "files": {
43
- "SKILL.md": "8be83688920875aa16566d20497b2a249a53de6f2fceeae463825154e064e502",
51
+ "SKILL.md": "724f6bc83467c28dc61a45161d3ffba7fecef2712eff3333bdf40d762731db44",
44
52
  "agents/openai.yaml": "1abab383deca54d32749fd9156f9a6211fe96e02e0166dc8a16ff8459ac31b74",
45
53
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
46
- "references/ledger-and-resume.md": "46bd4ad38eb70376d9ad36ac80269abc337c12e692e2b7de6483584f1003346a",
54
+ "references/ledger-and-resume.md": "5e3deae2af67735940564e04adcbc23c0c0dc86da2f6c80d68b8c1207c8f5900",
47
55
  "refresh/REFRESH.md": "7b854109d897b78b3055bf1ab89517afdca65c12eb65a4d009141e3ee11cf58f",
48
56
  "refresh/sources.json": "7bd883e5b3512e236a909ad6ff20c97f3555785867c31fd728256c7c2c514c74",
49
57
  "scripts/evidence-check.mjs": "9a360e6c5460d46b271b28457ee6be92f165c54c771c2dc6860c4a52e6890a9a",
@@ -52,8 +60,10 @@
52
60
  }
53
61
  },
54
62
  "dev-intake": {
63
+ "group": "dev-skills",
64
+ "repoOnly": false,
55
65
  "files": {
56
- "SKILL.md": "b5d89453176ad23c047fd38e38199c682c358df0468c9082edd8a6e34e6f229f",
66
+ "SKILL.md": "4754383297822d3f6e0ad8a391ac76499d83c74f424b90b928199d6c2649dc71",
57
67
  "agents/openai.yaml": "5473d2d3efffc518c07dfa8e6788c6535a5054ceca696119c5856ad265ef5581",
58
68
  "references/brief-template.md": "906c6b805f9737755f4d4b34fd16d4296924b95039379ea86bc59973949d9823",
59
69
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
@@ -63,8 +73,10 @@
63
73
  }
64
74
  },
65
75
  "dev-plan": {
76
+ "group": "dev-skills",
77
+ "repoOnly": false,
66
78
  "files": {
67
- "SKILL.md": "3c38b3f78f0f03d04370ea91aad99966fca5313fbe2919f44187d7c585c87866",
79
+ "SKILL.md": "eb2ce1542b1769fa6e7d9dc7be9fc9c84df8b353ded16e39e1e578ee372c1aca",
68
80
  "agents/openai.yaml": "4b9ccc9870b4eafb73c1bc4492a0b5a530097bbcbae11dbf3235160f4f05587b",
69
81
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
70
82
  "references/plan-format.md": "a915acce2fc447e685077756aeb94fdc3f84139800703034987b8409971493db",
@@ -74,8 +86,10 @@
74
86
  }
75
87
  },
76
88
  "dev-review": {
89
+ "group": "dev-skills",
90
+ "repoOnly": false,
77
91
  "files": {
78
- "SKILL.md": "00c1b86038ef8497480b440ee490019702013692c8ac5266a8340464745e0cf5",
92
+ "SKILL.md": "be32aa737cfd3309f7413407470ebffeedf6284f0f98cef6ae185c478c896c26",
79
93
  "agents/openai.yaml": "3f35d3dc60f179d4ac2526997c4d10173ba3018d06f0dbe77e209ce2978fa702",
80
94
  "assets/review-known-patterns.md.template": "2fe7dbcc4d0eeac542e7624111130265b2bb4f08806f20dbd2087842cbb22b5c",
81
95
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
@@ -87,8 +101,10 @@
87
101
  }
88
102
  },
89
103
  "dev-setup": {
104
+ "group": "dev-skills",
105
+ "repoOnly": false,
90
106
  "files": {
91
- "SKILL.md": "bb82e375b580954a5dae1044c2372e574a954bc8c14f79e4854aa8e39de98dc4",
107
+ "SKILL.md": "d8eb7741983998f56ff8aff52f66ec3887b7fd055c26cc232e1ff41da6fab7c1",
92
108
  "agents/openai.yaml": "be790c04fdf910e71642cfadf1a7c07c217bf7fad18581610feb4c31a79651f2",
93
109
  "assets/agents-section.md.template": "6c9591d50f73e1e05e78c8969c7836767432cf32487af7205b2cb9f4a410440e",
94
110
  "assets/dev-profile.md.template": "45b2a90fc7aee39e7b975151f1281787d6dd4d3b7d5f9df5b8f282d020a1d7a9",
@@ -100,8 +116,10 @@
100
116
  }
101
117
  },
102
118
  "dev-ship": {
119
+ "group": "dev-skills",
120
+ "repoOnly": false,
103
121
  "files": {
104
- "SKILL.md": "25cfb1eb2937cd33824f77cf008113406a54787b03edadc45762e688b1d3b5e0",
122
+ "SKILL.md": "aa3bd8b0a1ac076309190f2008896f24ebe1216352872ff428c1b88b55c06e65",
105
123
  "agents/openai.yaml": "8f18cfd505131557eb4c9f7b7f54e9069f787be4c162403fc30253eed49a06ea",
106
124
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
107
125
  "references/runbook.md": "5cbf3f89b2fcad1a2873f729b6c1a027566344d5dffcb799ed1d039e76b3e679",
@@ -111,8 +129,10 @@
111
129
  }
112
130
  },
113
131
  "dev-status": {
132
+ "group": "dev-skills",
133
+ "repoOnly": false,
114
134
  "files": {
115
- "SKILL.md": "42d1dfbda5602d68b114060c83cc9f61e14b9059d09a2b436218afdf0f009706",
135
+ "SKILL.md": "56415c2f69bb1a2fcb488b9846a20e632eb902b05c5b2b58ae9206d3eaffd77b",
116
136
  "agents/openai.yaml": "72435947f5afef0c8a2c170e987b14fe6057b174318c16b6b538ae8d129791fb",
117
137
  "references/conventions.md": "815f5189daff1b71b3251b7a57de183a4a387fcdedbd5875e673d5bd3de398fc",
118
138
  "refresh/REFRESH.md": "52aee9bae64d8342e127704d1db282e43ed57c101148727bf10172d6cb975bfa",
@@ -121,31 +141,35 @@
121
141
  }
122
142
  },
123
143
  "skill-maintainer": {
144
+ "group": "repo-tooling",
145
+ "repoOnly": true,
124
146
  "files": {
125
- "SKILL.md": "0947155a4e7c87d6f200a1e565013beac8e3093b17e3216d6901a20be863bb21",
147
+ "SKILL.md": "1733d2f1fa756cbb37c74ecee0d48ddd9abf31155c4dd72eba10af31c33d88ec",
126
148
  "agents/openai.yaml": "0b89d4e6416cf20b448f5322f3d7433cfde333d2b4a05b42ed53db3759701fb4",
127
- "references/release-ops.md": "14ef97980a81a4a767ad6888122a08e07f40602fd7673fcabf2d6af85799d4b0",
128
- "references/standards.md": "3646d0dd0560a87b333f70febf670b093c89b35111132cdb641b763bf184b02f",
129
- "refresh/REFRESH.md": "3955bff5f0a14c83336a73cdbd07ba18d9398996934af87fac4454206cd14023",
149
+ "references/release-ops.md": "b5df1922b34daf7751c332c019a0ea770a48283aaa2e25f83c9851bef47999b1",
150
+ "references/standards.md": "0dd7fcb6e2f0dd3aae7167227d7ec085481bc629aa027af35d48e5d0f7b9df86",
151
+ "refresh/REFRESH.md": "d3ba44be2c264b849d8a815111c049e782720428d5d0c5cea23998b05e50d58f",
130
152
  "refresh/sources.json": "4977152381d53555196ac676f95e61b4eaa301309a17560da5cda722010eb80f"
131
153
  }
132
154
  },
133
155
  "skillify": {
156
+ "group": "repo-tooling",
157
+ "repoOnly": true,
134
158
  "files": {
135
- "SKILL.md": "8469080261cf1911d054fb5b08cdec170566f5fcd07de6fa374d847d1b9d87aa",
159
+ "SKILL.md": "310ac32ff4d557cc166a470fd1866060dfc5f03898e8ff3da9c406b84580742b",
136
160
  "agents/openai.yaml": "df320cf32a8db06c213f7892844dab810b7e96cfbf32502d25185d4a4167ef87",
137
- "assets/templates/README.md.template": "68230ada702ad39f5de1f35e838ba590d89ca19b23868d4499353db6034fcc2c",
161
+ "assets/templates/README.md.template": "49dccb4e9b5bdb07c254f9142502b3fd11fd5b083d8661b0ae6a567dec79de74",
138
162
  "assets/templates/REFRESH.md.template": "ff86afade565d668333e7f6d0432213760fd68a62b8ba22897a8601b74c94ac0",
139
163
  "assets/templates/SKILL.md.template": "b038775f97b13b54227fe41a767a48cdaa922a93d1732f22c2d1d79bff52ca00",
140
164
  "assets/templates/openai.yaml.template": "2fbf488369825109d6cad4f17e7b729a40d9e3c43c7f01bc5dfa16361d59c086",
141
- "assets/templates/skill.test.ts.template": "7fead4f3a45a0b781bbe30b2686ca81bb432eb1ad92bf0d12b4db87ba998a32c",
165
+ "assets/templates/skill.test.ts.template": "9ca1e63ba6566350284c742e0e7958816d571fa57bd979b2c6a09104a0d9477c",
142
166
  "assets/templates/sources.json.template": "39449adcef33e6ffbc369a342bbfd1f299e9327157fe873991e1ed8d243288d1",
143
167
  "assets/templates/trigger-queries.json.template": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
144
168
  "references/authoring.md": "de61bc54140e07bb1910d5adac4e988e10fea9bd11b3e27234c0c699aa7bbf0d",
145
169
  "references/eval-playbook.md": "1f463dfea8d69b7022712776794369892643aa020186fe3a3707c493a6c2d4e9",
146
- "refresh/REFRESH.md": "fb74342bec10fbb8d28590dfde9147564346f2eba8976777073901a56e2742a8",
170
+ "refresh/REFRESH.md": "695f3283b27c14accf4095137898ce1f6dc40749483c5a5705e80aa9693f08c2",
147
171
  "refresh/sources.json": "e554e80c72c16dc70587361804db9e0cf2716c128820b2612d5983a009b2b242",
148
- "scripts/scaffold-skill.mjs": "108ffae496120777ee40414e058aa7e0cdfbfee81766f7124083bd36eaf1b727"
172
+ "scripts/scaffold-skill.mjs": "8bc3314f800fcff0bf5f797fdd82424ceb16a45ac03c4ee0eebcc416dfc41b97"
149
173
  }
150
174
  }
151
175
  }