agent-skillboard 0.1.0 → 0.1.1

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
@@ -232,6 +232,29 @@ top categories, next safe action, short section previews, and short action
232
232
  summaries. Use `skillboard brief --verbose` when you need the full list or
233
233
  full copyable command details.
234
234
 
235
+ ### Hermes System Prompt Bridge
236
+
237
+ Hermes does not automatically read `AGENTS.md` or `CLAUDE.md`. To make Hermes
238
+ follow the same SkillBoard policy, add a small bridge instruction to the Hermes
239
+ profile or system prompt for the project:
240
+
241
+ ```text
242
+ Use SkillBoard as the source of truth for agent skill availability.
243
+ Before answering what skills can be used, run:
244
+ skillboard brief --json --dir /path/to/your/project
245
+
246
+ Do not infer availability from installed SKILL.md files. Immediately before
247
+ invoking a skill, run:
248
+ skillboard guard use <skill-id> --workflow <workflow-name> --dir /path/to/your/project
249
+
250
+ For suggested policy changes, ask the user to approve one current action id,
251
+ then run:
252
+ skillboard apply-action <action-id> --dir /path/to/your/project --yes --json
253
+ ```
254
+
255
+ Replace `/path/to/your/project` and `<workflow-name>` with the project and
256
+ workflow Hermes should operate inside.
257
+
235
258
  Example: overloaded local agent profile
236
259
 
237
260
  ```text
package/docs/install.md CHANGED
@@ -144,6 +144,29 @@ primary flow. Raw `skillboard hook install ... --dry-run --json` previews and
144
144
  the matching non-dry-run command are underlying manual detail for operators who
145
145
  need to inspect or materialize an executable guard hook directly.
146
146
 
147
+ ## Hermes System Prompt Bridge
148
+
149
+ Hermes does not automatically read `AGENTS.md` or `CLAUDE.md`. If you want a
150
+ Hermes profile to follow SkillBoard policy, add this bridge to the profile or
151
+ system prompt for the managed project:
152
+
153
+ ```text
154
+ Use SkillBoard as the source of truth for agent skill availability.
155
+ Before answering what skills can be used, run:
156
+ skillboard brief --json --dir /path/to/your/project
157
+
158
+ Do not infer availability from installed SKILL.md files. Immediately before
159
+ invoking a skill, run:
160
+ skillboard guard use <skill-id> --workflow <workflow-name> --dir /path/to/your/project
161
+
162
+ For suggested policy changes, ask the user to approve one current action id,
163
+ then run:
164
+ skillboard apply-action <action-id> --dir /path/to/your/project --yes --json
165
+ ```
166
+
167
+ Use the workflow generated by init, such as `hermes-codex-local-manual`, or a
168
+ workflow you explicitly created for that Hermes profile.
169
+
147
170
  After installing a new local agent skill pack, plugin, workflow bundle, or
148
171
  harness, rescan before enabling anything:
149
172
 
@@ -8,7 +8,7 @@ source profiles, and the generated lockfile.
8
8
 
9
9
  ## Status
10
10
 
11
- Current package version: `0.1.0`
11
+ Current package version: `0.1.1`
12
12
 
13
13
  Current config schema version:
14
14
 
@@ -177,17 +177,29 @@ paths while project configs can still keep config-local paths.
177
177
  `skillboard lock write` refuses to write when verification has errors unless
178
178
  `--allow-unverified` is passed explicitly for investigation artifacts.
179
179
 
180
- ## Release Checklist
181
-
182
- Before tagging a public release:
183
-
184
- - Run `npm run check`.
185
- - Run `node --check` across `src`, `test`, and `bin`.
186
- - Run `npm pack --dry-run --json` and confirm internal artifacts are excluded.
187
- - Run at least one CLI smoke test through the public surface.
188
- - Confirm docs mention any breaking config, profile, or CLI changes.
189
- - Update package version and release notes.
190
-
180
+ ## Release Checklist
181
+
182
+ Before tagging a public release:
183
+
184
+ - Run `npm run check`.
185
+ - Run `node --check` across `src`, `test`, and `bin`.
186
+ - Run `npm pack --dry-run --json` and confirm internal artifacts are excluded.
187
+ - Run at least one CLI smoke test through the public surface.
188
+ - Confirm docs mention any breaking config, profile, or CLI changes.
189
+ - Update package version and release notes.
190
+ - Confirm npm Trusted Publisher is configured for package `agent-skillboard`,
191
+ repository `NyXXiR/skillboard`, workflow filename `publish.yml`, and allowed
192
+ action `npm publish`.
193
+ - If `npm trust github agent-skillboard --repo NyXXiR/skillboard --file
194
+ publish.yml --allow-publish` cannot configure it from the CLI, use the npm
195
+ package settings page and add a GitHub Actions trusted publisher with the
196
+ same repository, workflow filename, and allowed action.
197
+ - Push a version tag that exactly matches `package.json`, for example `v0.1.1`
198
+ for package version `0.1.1`.
199
+ - Let `.github/workflows/publish.yml` publish from the tag. The workflow runs
200
+ the full check suite, validates that the tag matches the package version, and
201
+ skips `npm publish` only when that exact version already exists on npm.
202
+
191
203
  For alpha releases, include a short "completion notes" section. Current
192
204
  completion notes:
193
205
 
@@ -199,3 +211,12 @@ completion notes:
199
211
  - dry-run plans report a capped YAML semantic change list rather than full patch
200
212
  hunks;
201
213
  - unusual YAML trivia may still be normalized during structured config writes.
214
+
215
+ ## 0.1.1 Completion Notes
216
+
217
+ - duplicate installed skill IDs are consolidated into one canonical skill entry
218
+ with duplicate source locations preserved as metadata;
219
+ - canonical duplicate skills can be shared by Codex and Hermes local workflows
220
+ without generating user-visible `-2` suffixes;
221
+ - README and install docs include a Hermes system prompt bridge for profiles
222
+ that do not automatically read `AGENTS.md` or `CLAUDE.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Policy and visibility layer for AI agent skills.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -125,6 +125,7 @@ export function mergeAgentSkillInventory(configText, inventory) {
125
125
  const reviewNotes = [];
126
126
  const managedSkillsByUnit = new Map();
127
127
  const localWorkflowSkillsByUnit = new Map();
128
+ const aliasSkillsByUnit = new Map();
128
129
 
129
130
  for (const skill of inventory.skills) {
130
131
  const unit = unitById.get(skill.ownerInstallUnit);
@@ -134,8 +135,10 @@ export function mergeAgentSkillInventory(configText, inventory) {
134
135
  skillsMap.set(skill.id, document.createNode(skillNode(skill, defaults)));
135
136
  addedSkills.push(skill.id);
136
137
  appendManagedSkill(managedSkillsByUnit, skill.ownerInstallUnit, skill.id);
138
+ appendSourceAliasSkills(aliasSkillsByUnit, skill.sourceAliases ?? [], skill.id);
137
139
  if (defaults.attachLocalWorkflow) {
138
140
  appendManagedSkill(localWorkflowSkillsByUnit, skill.ownerInstallUnit, skill.id);
141
+ appendLocalWorkflowAliasSkills(localWorkflowSkillsByUnit, skill.sourceAliases ?? [], skill.id);
139
142
  } else if (isTrustedLocalUserUnit(unit)) {
140
143
  reviewNotes.push(`Local skill ${skill.id} imported as manual-only candidate; use skillboard add workflow to attach it.`);
141
144
  }
@@ -144,24 +147,36 @@ export function mergeAgentSkillInventory(configText, inventory) {
144
147
  const owner = readYamlMapString(existing, "owner_install_unit", "");
145
148
  if (owner === skill.ownerInstallUnit) {
146
149
  appendManagedSkill(managedSkillsByUnit, skill.ownerInstallUnit, skill.id);
150
+ appendSourceAliases(existing, skill.sourceAliases ?? [], document);
151
+ appendSourceAliasSkills(aliasSkillsByUnit, skill.sourceAliases ?? [], skill.id);
152
+ if (defaults.attachLocalWorkflow) {
153
+ appendManagedSkill(localWorkflowSkillsByUnit, skill.ownerInstallUnit, skill.id);
154
+ appendLocalWorkflowAliasSkills(localWorkflowSkillsByUnit, skill.sourceAliases ?? [], skill.id);
155
+ }
147
156
  } else {
148
- skippedSkills.push(skill.id);
157
+ const alias = sourceAliasForSkill(skill);
158
+ appendSourceAliases(existing, [alias], document);
159
+ appendManagedSkill(aliasSkillsByUnit, skill.ownerInstallUnit, skill.id);
160
+ if (defaults.attachLocalWorkflow) {
161
+ appendManagedSkill(localWorkflowSkillsByUnit, skill.ownerInstallUnit, skill.id);
162
+ }
149
163
  }
150
164
  }
151
165
 
152
166
  for (const unit of inventory.installUnits) {
153
167
  const managedSkills = managedSkillsByUnit.get(unit.id) ?? [];
154
- if (managedSkills.length === 0 && !hasRuntimeComponents(unit)) {
168
+ const aliasSkills = aliasSkillsByUnit.get(unit.id) ?? [];
169
+ if (managedSkills.length === 0 && aliasSkills.length === 0 && !hasRuntimeComponents(unit)) {
155
170
  continue;
156
171
  }
157
172
  const existing = unitsMap.get(unit.id, true);
158
173
  if (existing === undefined) {
159
- unitsMap.set(unit.id, document.createNode(installUnitNode(unit, managedSkills)));
174
+ unitsMap.set(unit.id, document.createNode(installUnitNode(unit, managedSkills, aliasSkills)));
160
175
  addedInstallUnits.push(unit.id);
161
176
  continue;
162
177
  }
163
178
  const unitMap = requireYamlMap(existing, `install_units.${unit.id}`);
164
- if (mergeInstallUnitNode(unitMap, unit, managedSkills, document)) {
179
+ if (mergeInstallUnitNode(unitMap, unit, managedSkills, aliasSkills, document)) {
165
180
  updatedInstallUnits.push(unit.id);
166
181
  }
167
182
  }
@@ -367,11 +382,13 @@ async function findPluginManifests(root) {
367
382
  async function inventoryFromGroups(groups, home, initialWarnings = []) {
368
383
  const skills = [];
369
384
  const installUnits = [];
370
- const usedSkillIds = new Set();
385
+ const skillById = new Map();
371
386
  const warnings = [...initialWarnings];
387
+ let scannedSkills = 0;
372
388
 
373
- for (const group of groups.sort((left, right) => left.unit.id.localeCompare(right.unit.id))) {
389
+ for (const group of groups.sort(compareInventoryGroups)) {
374
390
  const unitSkills = [];
391
+ const aliasSkills = [];
375
392
  for (const file of group.files.sort((left, right) => left.localeCompare(right))) {
376
393
  let frontmatter;
377
394
  try {
@@ -380,30 +397,57 @@ async function inventoryFromGroups(groups, home, initialWarnings = []) {
380
397
  warnings.push(`skill file ${displayPath(file, home)} skipped: ${errorMessage(error)}`);
381
398
  continue;
382
399
  }
383
- const baseId = skillIdFor(group.unit, group.root, file, frontmatter);
384
- const id = uniqueId(baseId, usedSkillIds);
385
- usedSkillIds.add(id);
400
+ scannedSkills += 1;
401
+ const id = skillIdFor(group.unit, group.root, file, frontmatter);
402
+ const sourceAlias = {
403
+ ownerInstallUnit: group.unit.id,
404
+ path: skillPath(group.root, file)
405
+ };
406
+ const existing = skillById.get(id);
407
+ if (existing !== undefined) {
408
+ existing.sourceAliases.push(sourceAlias);
409
+ aliasSkills.push(id);
410
+ continue;
411
+ }
386
412
  unitSkills.push(id);
387
- skills.push({
413
+ const skill = {
388
414
  id,
389
- path: skillPath(group.root, file),
415
+ path: sourceAlias.path,
390
416
  status: "quarantined",
391
417
  invocation: "blocked",
392
418
  exposure: "exported",
393
419
  category: group.unit.category,
394
420
  ownerInstallUnit: group.unit.id,
395
- description: frontmatter.description
396
- });
421
+ description: frontmatter.description,
422
+ sourceAliases: []
423
+ };
424
+ skillById.set(id, skill);
425
+ skills.push(skill);
397
426
  }
398
427
  if (unitSkills.length > 0) {
399
428
  group.unit.providedComponents = providedComponentList({ ...group.unit, skills: unitSkills });
400
429
  }
401
- if (unitSkills.length > 0 || hasRuntimeComponents(group.unit)) {
402
- installUnits.push(installUnitWithSkills(group.unit, unitSkills, group.root, home));
430
+ if (unitSkills.length > 0 || aliasSkills.length > 0 || hasRuntimeComponents(group.unit)) {
431
+ installUnits.push(installUnitWithSkills(group.unit, unitSkills, aliasSkills, group.root, home));
403
432
  }
404
433
  }
405
434
 
406
- return { skills, installUnits, warnings };
435
+ return { skills, installUnits, scannedSkills, warnings };
436
+ }
437
+
438
+ function compareInventoryGroups(left, right) {
439
+ const rank = inventoryUnitRank(left.unit) - inventoryUnitRank(right.unit);
440
+ return rank === 0 ? left.unit.id.localeCompare(right.unit.id) : rank;
441
+ }
442
+
443
+ function inventoryUnitRank(unit) {
444
+ if (isTrustedLocalUserUnit(unit)) {
445
+ return 0;
446
+ }
447
+ if (unit.kind === "skill") {
448
+ return 1;
449
+ }
450
+ return 2;
407
451
  }
408
452
 
409
453
  function normalizeDiscoveryResult(result) {
@@ -419,7 +463,7 @@ function normalizeDiscoveryResult(result) {
419
463
  return { groups: [], warnings: [] };
420
464
  }
421
465
 
422
- function installUnitWithSkills(unit, skills, root, home) {
466
+ function installUnitWithSkills(unit, skills, aliasSkills, root, home) {
423
467
  const commands = unit.commands ?? [];
424
468
  const hooks = unit.hooks ?? [];
425
469
  const mcpServers = unit.mcpServers ?? [];
@@ -433,7 +477,8 @@ function installUnitWithSkills(unit, skills, root, home) {
433
477
  mcpServers,
434
478
  modifiedConfigFiles: unit.modifiedConfigFiles ?? [],
435
479
  permissionRisk: unit.permissionRisk ?? permissionRiskFor({ commands, hooks, mcpServers }),
436
- skills
480
+ skills,
481
+ sourceAliasSkills: aliasSkills
437
482
  };
438
483
  }
439
484
 
@@ -495,17 +540,18 @@ function skillPath(root, file) {
495
540
  }
496
541
 
497
542
  function skillNode(skill, defaults = {}) {
498
- return {
543
+ return stripUndefined({
499
544
  path: skill.path,
500
545
  status: defaults.status ?? skill.status,
501
546
  invocation: defaults.invocation ?? skill.invocation,
502
547
  exposure: skill.exposure,
503
548
  category: skill.category,
504
- owner_install_unit: skill.ownerInstallUnit
505
- };
549
+ owner_install_unit: skill.ownerInstallUnit,
550
+ source_aliases: sourceAliasesNode(skill.sourceAliases ?? [])
551
+ });
506
552
  }
507
553
 
508
- function installUnitNode(unit, skills) {
554
+ function installUnitNode(unit, skills, aliasSkills = []) {
509
555
  return stripUndefined({
510
556
  kind: unit.kind,
511
557
  source_class: unit.sourceClass,
@@ -518,6 +564,7 @@ function installUnitNode(unit, skills) {
518
564
  provided_components: unit.providedComponents,
519
565
  components: {
520
566
  skills,
567
+ source_aliases: aliasSkills,
521
568
  commands: unit.commands,
522
569
  hooks: unit.hooks,
523
570
  mcp_servers: unit.mcpServers
@@ -530,10 +577,11 @@ function installUnitNode(unit, skills) {
530
577
  });
531
578
  }
532
579
 
533
- function mergeInstallUnitNode(unitMap, unit, skills, document) {
580
+ function mergeInstallUnitNode(unitMap, unit, skills, aliasSkills, document) {
534
581
  let changed = false;
535
582
  const components = ensureNestedMap(unitMap, "components", document);
536
583
  changed = appendNestedSequenceValues(components, "skills", skills, document) || changed;
584
+ changed = appendNestedSequenceValues(components, "source_aliases", aliasSkills, document) || changed;
537
585
  changed = appendNestedSequenceValues(components, "commands", unit.commands ?? [], document) || changed;
538
586
  changed = appendNestedSequenceValues(components, "hooks", unit.hooks ?? [], document) || changed;
539
587
  changed = appendNestedSequenceValues(components, "mcp_servers", unit.mcpServers ?? [], document) || changed;
@@ -566,6 +614,64 @@ function appendManagedSkill(map, unitId, skillId) {
566
614
  map.set(unitId, values);
567
615
  }
568
616
 
617
+ function appendSourceAliasSkills(map, sourceAliases, skillId) {
618
+ for (const alias of sourceAliases) {
619
+ appendManagedSkill(map, alias.ownerInstallUnit, skillId);
620
+ }
621
+ }
622
+
623
+ function appendLocalWorkflowAliasSkills(map, sourceAliases, skillId) {
624
+ for (const alias of sourceAliases) {
625
+ appendManagedSkill(map, alias.ownerInstallUnit, skillId);
626
+ }
627
+ }
628
+
629
+ function sourceAliasForSkill(skill) {
630
+ return {
631
+ ownerInstallUnit: skill.ownerInstallUnit,
632
+ path: skill.path
633
+ };
634
+ }
635
+
636
+ function sourceAliasesNode(sourceAliases) {
637
+ const aliases = sourceAliases
638
+ .filter((alias) => alias.ownerInstallUnit !== undefined && alias.path !== undefined)
639
+ .map((alias) => ({
640
+ owner_install_unit: alias.ownerInstallUnit,
641
+ path: alias.path
642
+ }));
643
+ return aliases.length === 0 ? undefined : aliases;
644
+ }
645
+
646
+ function appendSourceAliases(skillNodeValue, sourceAliases, document) {
647
+ const skillMap = requireYamlMap(skillNodeValue, "skills.<id>");
648
+ const validAliases = sourceAliases.filter((alias) => alias.ownerInstallUnit !== undefined && alias.path !== undefined);
649
+ if (validAliases.length === 0) {
650
+ return false;
651
+ }
652
+ const seq = ensureNestedSeq(skillMap, "source_aliases", document);
653
+ const before = seq.items.length;
654
+ for (const alias of validAliases) {
655
+ if (sourceAliasExists(seq, alias)) {
656
+ continue;
657
+ }
658
+ seq.add(document.createNode({
659
+ owner_install_unit: alias.ownerInstallUnit,
660
+ path: alias.path
661
+ }));
662
+ }
663
+ return seq.items.length !== before;
664
+ }
665
+
666
+ function sourceAliasExists(seq, alias) {
667
+ return seq.items.some((item) => {
668
+ if (!YAML.isMap(item)) {
669
+ return false;
670
+ }
671
+ return item.get("owner_install_unit") === alias.ownerInstallUnit && item.get("path") === alias.path;
672
+ });
673
+ }
674
+
569
675
  async function manifestMcpServers(value, pluginRoot) {
570
676
  if (typeof value === "string") {
571
677
  const path = resolve(pluginRoot, value);
@@ -737,17 +843,6 @@ function preserveLineEndings(text, reference) {
737
843
  return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
738
844
  }
739
845
 
740
- function uniqueId(base, used) {
741
- if (!used.has(base)) {
742
- return base;
743
- }
744
- let counter = 2;
745
- while (used.has(`${base}-${counter}`)) {
746
- counter += 1;
747
- }
748
- return `${base}-${counter}`;
749
- }
750
-
751
846
  function validId(value) {
752
847
  return /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value);
753
848
  }
@@ -26,7 +26,7 @@ export async function refreshAgentInventory(options = {}) {
26
26
  changed: plan.changed,
27
27
  plan,
28
28
  scan: {
29
- scannedSkills: inventory.skills.length,
29
+ scannedSkills: inventory.scannedSkills ?? inventory.skills.length,
30
30
  scannedInstallUnits: inventory.installUnits.length,
31
31
  addedSkills: merged.addedSkills,
32
32
  addedInstallUnits: merged.addedInstallUnits,