agent-skillboard 0.3.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.1 — 2026-07-14
6
+
7
+ ### Added
8
+
9
+ - Added read-only installation health to `doctor` and `status`, including the
10
+ running package, PATH-selected executable, shadowing, and duplicate npm
11
+ installs without executing candidate programs.
12
+ - Added `skillboard setup --agent <agent> --skill-root <path>` for registering a
13
+ nonstandard user skill root and reusing it across later setup, package update,
14
+ share, unshare, and user-uninstall runs.
15
+
16
+ ### Changed
17
+
18
+ - Made setup and global package updates preserve version 1 policy bytes and
19
+ print only the explicit v2 migration preview command.
20
+ - Made `setup` the idempotent install/update/reconcile entrypoint for agents and
21
+ Hermes profiles added after SkillBoard installation.
22
+ - Made setup backfill only already-selected `shared: true` skills into newly
23
+ active compatible roots while preserving agent-local defaults and unmanaged
24
+ collisions.
25
+ - Kept registered roots in generated operational state rather than expanding
26
+ the v2 policy beyond `enabled`, `shared`, and optional preference.
27
+
28
+ ### Fixed
29
+
30
+ - Added actionable single-prefix recovery guidance so system npm and Node
31
+ version-manager installations cannot silently drift without a doctor warning.
32
+ - Prevented a stale default-root copy from producing false guard availability
33
+ after a custom root is registered.
34
+ - Preserved conventional roots that still contain agent-owned skills alongside
35
+ registered roots, and included historical managed guidance in clean user
36
+ removal.
37
+ - Restored invoking-user ownership recursively for shared skill copies created
38
+ or repaired by setup during sudo-driven global installs and updates.
39
+
40
+ ### Security
41
+
42
+ - Restricted registered roots to non-symlinked directories inside the invoking
43
+ user's home and rejected assigning one root to different agents.
44
+ - Treated malformed or symlinked share markers as unmanaged evidence so setup
45
+ and removal preserve user-owned or externally linked content.
46
+
5
47
  ## 0.3.0 — 2026-07-13
6
48
 
7
49
  ### Changed
package/README.md CHANGED
@@ -9,7 +9,7 @@ Valid installed skills default to enabled and agent-local. Sharing is opt-in per
9
9
  skill. Optional preference ranks enabled skills installed for the current agent
10
10
  and never changes availability or copies files.
11
11
 
12
- Status: public alpha. Package 0.3.0 writes policy schema v2.
12
+ Status: public alpha. Package 0.3.1 writes policy schema v2.
13
13
 
14
14
  <p align="center">
15
15
  <img src="https://raw.githubusercontent.com/NyXXiR/skillboard/main/skillboard.png" alt="SkillBoard architecture diagram" width="100%">
@@ -28,6 +28,7 @@ Install globally:
28
28
 
29
29
  ```bash
30
30
  npm install -g agent-skillboard
31
+ skillboard doctor --summary
31
32
  ```
32
33
 
33
34
  The package postinstall sets up detected agent guidance and creates the
@@ -39,6 +40,38 @@ normal install. If lifecycle scripts were skipped or another agent was added:
39
40
  skillboard setup --agent codex,claude,opencode,hermes --yes
40
41
  ```
41
42
 
43
+ Use the same Node/npm environment for later updates:
44
+
45
+ ```bash
46
+ npm config get prefix
47
+ npm install -g agent-skillboard@latest
48
+ skillboard doctor --summary
49
+ ```
50
+
51
+ Doctor compares the running package with the `skillboard` executable selected
52
+ by `PATH` and reports duplicate global installs without executing those
53
+ candidates. If multiple SkillBoard installations are reported, choose one npm
54
+ prefix, activate the Node environment that owns each stale copy, and run
55
+ `npm uninstall -g agent-skillboard` there. SkillBoard does not automatically
56
+ uninstall another prefix. Restart or refresh agents after an update because
57
+ some agents cache user skills.
58
+
59
+ `setup` is safe to rerun. It refreshes managed guidance and inventory, discovers
60
+ late standard agent homes and Hermes profiles, and fills already-selected
61
+ `shared: true` skills into newly active roots. It does not make every skill
62
+ global and never overwrites an unmanaged skill.
63
+
64
+ For an agent that uses a nonstandard skill directory, register that root once:
65
+
66
+ ```bash
67
+ skillboard setup --agent hermes --skill-root ~/.hermes/profiles/work/skills --yes
68
+ ```
69
+
70
+ The root must be inside the invoking user's home. SkillBoard records it as
71
+ operational discovery state in `~/.skillboard/agent-roots.json`, then reuses it
72
+ on later setup and global package updates. This does not add another policy
73
+ scope; per-skill `shared` remains the only cross-agent sharing decision.
74
+
42
75
  `sudo npm install -g agent-skillboard` is supported when system npm requires
43
76
  it. Setup resolves `SUDO_USER` and restores managed home files to that user.
44
77
  Setup and ordinary use write no project policy and require no project init.
@@ -124,7 +157,9 @@ skillboard migrate v2 --config <path> --yes --json
124
157
  skillboard migrate v2 --config <path> --rollback <backup> --json
125
158
  ```
126
159
 
127
- All v1 mutations refuse and point to migration. v0.4.0 removes the v1 reader.
160
+ Setup and package updates keep v1 bytes unchanged and print the preview command.
161
+ SkillBoard does not automatically migrate version 1. All v1 mutations refuse
162
+ and point to migration. v0.4.0 removes the v1 reader.
128
163
 
129
164
  ## Cleanup
130
165
 
@@ -19,7 +19,7 @@ if (!shouldAutoSetup(process.env)) {
19
19
  "Run skillboard setup later after adding another supported agent:",
20
20
  " skillboard setup",
21
21
  "",
22
- "Setup only writes user agent skill files. It does not create skillboard.config.yaml or .skillboard/.",
22
+ "Setup writes user-agent guidance, reconciles shared skills, and refreshes the user-level policy and inventory.",
23
23
  "skillboard init is deprecated project-local policy bootstrap and is not needed for normal use.",
24
24
  ""
25
25
  ].join("\n")}`);
@@ -43,6 +43,7 @@ try {
43
43
  });
44
44
  if (exitCode === 0) {
45
45
  process.stderr.write("Agent setup complete. Package updates rerun this setup automatically; run skillboard setup later after adding another supported agent. No project init was run.\n");
46
+ process.stderr.write("Run skillboard doctor --summary to check policy and executable paths after the update.\n");
46
47
  } else {
47
48
  process.stderr.write("Agent setup did not find supported agent homes. Run skillboard setup after installing or enabling a supported agent.\n");
48
49
  }
@@ -38,6 +38,13 @@ remove or quarantine copies that the user or another tool installed. With
38
38
  managed copies for supported agents, while preserving every agent-owned
39
39
  original.
40
40
 
41
+ `setup` is a repeatable user-level convergence operation. It observes agents
42
+ and Hermes profiles added after installation, refreshes managed guidance and
43
+ inventory, and materializes missing copies only for skills already marked
44
+ `shared: true`. Custom agent roots are remembered outside policy as operational
45
+ discovery state. They must not introduce project scope, source trust gates, or
46
+ another authorization layer.
47
+
41
48
  ## Development rule
42
49
 
43
50
  Every route, brief, guard, dashboard, generated bridge, and policy mutation must
package/docs/install.md CHANGED
@@ -5,6 +5,7 @@
5
5
  ```bash
6
6
  npm install -g agent-skillboard
7
7
  skillboard --version
8
+ skillboard doctor --summary
8
9
  ```
9
10
 
10
11
  Postinstall detects supported agent homes, installs managed guidance, creates
@@ -12,13 +13,55 @@ Postinstall detects supported agent homes, installs managed guidance, creates
12
13
  separate setup command is required after a normal global install or update, and
13
14
  no project init is needed.
14
15
 
15
- Run setup later only when lifecycle scripts were skipped or another agent was
16
- added:
16
+ ## Safe updates and npm prefixes
17
+
18
+ Run the update with the Node/npm environment whose global prefix should own the
19
+ CLI:
20
+
21
+ ```bash
22
+ npm config get prefix
23
+ npm install -g agent-skillboard@latest
24
+ skillboard doctor --summary
25
+ ```
26
+
27
+ Node version managers and system npm can use different global prefixes. Doctor
28
+ reports the current package, the `skillboard` executable selected by `PATH`, and
29
+ multiple SkillBoard installations. Discovery reads executable links and package
30
+ metadata only; it does not execute PATH candidates.
31
+
32
+ When doctor reports duplicate global installs, select the prefix you intend to
33
+ keep. Activate the Node environment that owns each stale prefix, confirm it with
34
+ `npm config get prefix`, and run `npm uninstall -g agent-skillboard` there.
35
+ SkillBoard does not automatically uninstall another prefix or request elevated
36
+ permissions for cleanup.
37
+
38
+ Run setup later when lifecycle scripts were skipped, another agent or Hermes
39
+ profile was added, or a managed root needs to be repaired:
17
40
 
18
41
  ```bash
19
42
  skillboard setup --agent codex,claude,opencode,hermes --yes
20
43
  ```
21
44
 
45
+ Setup is idempotent. Each run refreshes managed agent guidance, user policy,
46
+ and observed inventory. If the user already chose `shared: true` for a skill,
47
+ setup creates only the missing compatible managed copies for newly discovered
48
+ agent roots. It preserves unmanaged files and does not share agent-local skills.
49
+ Restart or refresh agents after setup or an npm update because an agent may
50
+ cache its user skill inventory.
51
+
52
+ Register a nonstandard agent skill directory once with exactly one agent:
53
+
54
+ ```bash
55
+ skillboard setup --agent hermes --skill-root ~/.hermes/profiles/work/skills --yes
56
+ ```
57
+
58
+ The root must remain inside the invoking user's home and must not traverse a
59
+ symbolic link. SkillBoard stores it in `~/.skillboard/agent-roots.json` as
60
+ operational discovery state, not policy, and reuses it during setup and global
61
+ package updates. Conventional roots that contain agent-owned skills remain
62
+ active; obsolete roots containing only SkillBoard-managed artifacts do not
63
+ create false agent presence.
64
+
22
65
  System npm may require `sudo npm install -g agent-skillboard`. SkillBoard uses
23
66
  `SUDO_USER` for user-level state and restores managed files to that user.
24
67
 
@@ -62,6 +105,9 @@ skillboard migrate v2 --config <path> --rollback <backup> --json
62
105
  ```
63
106
 
64
107
  Version 1 is read-only during v0.3.x. v0.4.0 removes the v1 reader.
108
+ Setup and postinstall preserve an existing v1 policy byte-for-byte and suggest
109
+ the preview command only. SkillBoard does not automatically migrate version 1;
110
+ apply and rollback remain explicit user actions.
65
111
 
66
112
  ## Uninstall
67
113
 
package/docs/reference.md CHANGED
@@ -13,7 +13,7 @@ Source tree: replace `skillboard ` with `node bin/skillboard.mjs `.
13
13
  ## Core user-level commands
14
14
 
15
15
  ```text
16
- skillboard setup [--yes] [--agent codex[,claude,opencode,hermes]]
16
+ skillboard setup [--yes] [--agent codex[,claude,opencode,hermes]] [--skill-root <path>]
17
17
  skillboard inventory refresh [--config <path>] [--dry-run] [--json]
18
18
  skillboard brief [--agent codex|claude|opencode|hermes] [--intent <request>] [--include-actions] [--json]
19
19
  skillboard route <intent> --agent codex|claude|opencode|hermes [--json]
@@ -33,6 +33,22 @@ Without path overrides, commands read `~/skillboard.config.yaml` and
33
33
  `~/.skillboard/inventory.json` from any directory. Setup bootstraps both files
34
34
  atomically and never creates project state.
35
35
 
36
+ Doctor also reports installation health: the running package version and
37
+ entrypoint, the `skillboard` executable selected by `PATH`, discovered package
38
+ installations, shadowing, and duplicate global installs. These observations are
39
+ informational and do not change policy health. Candidate executables are never
40
+ run during discovery. Use `skillboard doctor --summary` after updates; use
41
+ `npm config get prefix` in each Node environment to identify the owner of a
42
+ stale global copy. SkillBoard does not automatically uninstall another prefix.
43
+
44
+ Setup is the convergent install/update command. It refreshes managed guidance,
45
+ policy, and inventory; discovers late standard roots and Hermes profiles; and
46
+ creates missing compatible copies for policy entries already marked
47
+ `shared: true`. `--skill-root` requires exactly one `--agent`, accepts only a
48
+ non-symlinked path inside the invoking user's home, and persists the mapping in
49
+ `~/.skillboard/agent-roots.json`. That registry is operational discovery state,
50
+ not authorization or policy. One root cannot be registered to two agents.
51
+
36
52
  Guard requires a valid inventory record, `enabled: true`, and the selected agent
37
53
  in `installed_on`. Optional preference ranks candidates but never changes
38
54
  availability.
@@ -137,6 +153,9 @@ Preview changes no bytes. Apply creates an adjacent byte-for-byte backup and
137
153
  writes v2 policy plus generated inventory atomically. Rollback restores the
138
154
  selected backup. v0.4.0 removes the v1 reader.
139
155
 
156
+ Setup and npm postinstall preserve an existing v1 policy and print only the
157
+ preview form. SkillBoard does not automatically migrate version 1.
158
+
140
159
  Legacy fields are interpreted only by migration and never become hidden v2
141
160
  authorization. Primary examples are `examples/v2-multi-source.config.yaml` and
142
161
  `examples/v2-policy-error.config.yaml`; unprefixed v1 examples remain migration
package/docs/user-flow.md CHANGED
@@ -18,6 +18,19 @@ agents. No project init is needed. If setup was skipped:
18
18
  skillboard setup --yes --agent codex
19
19
  ```
20
20
 
21
+ If an agent is installed later, rerun the same command for that agent. Setup
22
+ discovers standard homes and Hermes profiles, installs guidance, refreshes
23
+ inventory, and backfills only skills the user already selected with
24
+ `shared: true`. For a custom location:
25
+
26
+ ```bash
27
+ skillboard setup --agent hermes --skill-root ~/.hermes/profiles/work/skills --yes
28
+ ```
29
+
30
+ The registered root survives later setup and package-update runs. No project
31
+ init, profile-specific policy layer, or new approval per already-shared skill is
32
+ introduced. Unmanaged collisions are preserved and reported.
33
+
21
34
  ## 2. Observe and route
22
35
 
23
36
  From any directory, the agent reads:
@@ -22,6 +22,10 @@ Preview is non-mutating, apply creates an adjacent byte-for-byte backup, and
22
22
  rollback restores the selected backup. Old action ids, hooks, and lock
23
23
  projections are invalid after migration.
24
24
 
25
+ Package install, update, setup, and doctor never automatically migrate a v1
26
+ policy. Setup preserves its bytes and prints the preview command so the policy
27
+ decision stays separate from package maintenance.
28
+
25
29
  ## Versioned v2 contract
26
30
 
27
31
  Policy availability requires `enabled: true` and generated installation presence
@@ -34,12 +38,21 @@ schema.
34
38
 
35
39
  1. Run `npm run check`.
36
40
  2. Run `npm pack --dry-run --json` and inspect public contents.
37
- 3. Confirm `CHANGELOG.md` includes the package version.
38
- 4. Create a matching `vX.Y.Z` tag.
39
- 5. `.github/workflows/publish.yml` verifies the tag exactly matches
41
+ 3. Install the tarball into an isolated prefix and run
42
+ `skillboard doctor --summary`; verify fresh setup, v1 preservation, and a
43
+ late-agent shared-skill reconciliation.
44
+ 4. Confirm `CHANGELOG.md` includes the package version.
45
+ 5. Push `main` and wait for the complete cross-platform check matrix.
46
+ 6. Create a matching `vX.Y.Z` tag on that exact green commit.
47
+ 7. `.github/workflows/publish.yml` verifies the tag exactly matches
40
48
  `package.json`, configures the registry URL in `setup-node`, then runs
41
49
  `npm publish` with provenance.
42
50
 
51
+ After publish, verify the registry version from a clean install. On development
52
+ machines with system npm plus a Node version manager, use `npm config get
53
+ prefix` and doctor to detect multiple SkillBoard installations; release tooling
54
+ never automatically uninstalls another prefix.
55
+
43
56
  Publishing uses `NPM_TOKEN` through `NODE_AUTH_TOKEN`. Prefer npm trusted
44
57
  publishing with OIDC when available. Release automation skips `npm publish`
45
58
  only when that exact version already exists on npm.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Keep agent skills broadly available while routing overlaps consistently.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -2,17 +2,29 @@ import { isAbsolute, relative, resolve } from "node:path";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { createInterface } from "node:readline";
4
4
  import { installAgentIntegration, uninstallAgentIntegration } from "./agent-integration-files.mjs";
5
- import { applyOwnership, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
5
+ import { applyOwnership, applyOwnershipTree, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
6
6
  import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
7
+ import {
8
+ agentRootRegistryPath,
9
+ loadRegisteredAgentRoots,
10
+ mergeRegisteredAgentRoots,
11
+ proposedAgentRoot,
12
+ writeRegisteredAgentRoots
13
+ } from "./agent-root-registry.mjs";
7
14
  import { refreshAgentInventory } from "./inventory-refresh.mjs";
15
+ import { reconcileSharedSkills } from "./shared-skill-reconcile.mjs";
8
16
 
9
17
  export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
18
+ assertSetupOptions(options);
10
19
  if (options.get("dir") !== undefined) {
11
20
  throw new Error("skillboard setup is agent-layer setup and does not accept --dir");
12
21
  }
13
22
  const env = runtime.env ?? process.env;
14
23
  const home = await resolveSetupHome(env, runtime);
15
- const targets = await agentSetupTargets(options, runtime, home);
24
+ const existingRoots = await loadRegisteredAgentRoots(home);
25
+ const proposedRoot = await setupSkillRoot(options, home, runtime.cwd ?? process.cwd());
26
+ const registeredRoots = mergeRegisteredAgentRoots(existingRoots, proposedRoot);
27
+ const targets = await agentSetupTargets(options, runtime, home, registeredRoots);
16
28
  if (targets.length === 0) {
17
29
  stdout.write("No supported agent user skill roots were detected.\n");
18
30
  stdout.write(`Create a supported agent home or pass --agent ${supportedAgentNames().join(",")} to choose targets.\n`);
@@ -32,18 +44,49 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
32
44
  const ownership = setupOwnership(env, runtime, home);
33
45
  const result = await installAgentIntegration(targets, ownership);
34
46
  await mkdir(home, { recursive: true });
35
- const inventory = await refreshAgentInventory({ root: home, home, env });
36
- await applyOwnership(resolve(home, inventory.configPath), ownership);
37
- if (inventory.inventoryPath !== null) await applyOwnership(resolve(home, inventory.inventoryPath), ownership);
47
+ let inventory = await refreshAgentInventory({
48
+ root: home,
49
+ home,
50
+ env,
51
+ registeredRoots,
52
+ preserveLegacyPolicy: true
53
+ });
54
+ const configPath = resolve(home, inventory.configPath);
55
+ const inventoryPath = inventory.inventoryPath === null ? null : resolve(home, inventory.inventoryPath);
56
+ const shared = inventoryPath === null
57
+ ? { created: [], unchanged: [], preserved: [], blocked: [] }
58
+ : await reconcileSharedSkills({ home, env, targets, configPath, inventoryPath });
59
+ if (shared.created.length > 0) {
60
+ inventory = await refreshAgentInventory({ root: home, home, env, registeredRoots });
61
+ }
62
+ for (const entry of [...shared.created, ...shared.unchanged]) {
63
+ await applyOwnershipTree(entry.path, ownership);
64
+ }
65
+ if (proposedRoot !== undefined) {
66
+ await writeRegisteredAgentRoots(home, registeredRoots);
67
+ await applyOwnership(agentRootRegistryPath(home), ownership);
68
+ }
69
+ await applyOwnership(configPath, ownership);
70
+ if (inventoryPath !== null) await applyOwnership(inventoryPath, ownership);
38
71
  stdout.write("SkillBoard agent integration installed.\n");
39
72
  writeList(stdout, "Created", result.created);
40
73
  writeList(stdout, "Updated", result.updated);
41
74
  writeList(stdout, "Unchanged", result.unchanged);
42
75
  writeList(stdout, "Preserved", result.preserved);
76
+ if (proposedRoot !== undefined) stdout.write(`Registered agent roots: ${registeredRoots.length}\n`);
77
+ if (shared.created.length > 0) stdout.write(`Created shared copies: ${shared.created.length}\n`);
78
+ if (shared.unchanged.length > 0) stdout.write(`Unchanged shared copies: ${shared.unchanged.length}\n`);
79
+ writeList(stdout, "Preserved shared copies", shared.preserved.map(formatSharedEntry));
80
+ writeList(stdout, "Blocked shared copies", shared.blocked.map(formatBlockedEntry));
43
81
  stdout.write(`User policy: ${inventory.configPath}\n`);
44
82
  stdout.write(`Observed skills: ${inventory.scan.scannedSkills}\n`);
83
+ if (inventory.inventoryPath === null) {
84
+ stdout.write("Policy version 1 remains read-only during SkillBoard v0.3.x.\n");
85
+ stdout.write(`Preview migration: ${commandPrefix(runtime)} migrate v2 --config ${shellQuote(configPath, runtime.platform)} --json\n`);
86
+ }
45
87
  stdout.write("Next:\n");
46
88
  stdout.write("- Restart or refresh agents that cache user skills.\n");
89
+ stdout.write("- Run skillboard doctor --summary to check policy and executable paths.\n");
47
90
  stdout.write("- User-level policy and inventory were refreshed; no project was initialized.\n");
48
91
  stdout.write('- Ask the agent in a workspace: "Review this plan and point out weak assumptions."\n');
49
92
  stdout.write("- SkillBoard will step in when skills overlap, routing is ambiguous, or you ask for a skill decision.\n");
@@ -136,7 +179,7 @@ function defaultRuntime() {
136
179
  };
137
180
  }
138
181
 
139
- async function agentSetupTargets(options, runtime, setupHome) {
182
+ async function agentSetupTargets(options, runtime, setupHome, registeredRoots) {
140
183
  const env = runtime.env ?? process.env;
141
184
  const home = setupHome ?? await resolveSetupHome(env, runtime);
142
185
  const requested = readCsv(options.get("agent"));
@@ -147,20 +190,48 @@ async function agentSetupTargets(options, runtime, setupHome) {
147
190
  if (!supported.has(name)) {
148
191
  throw new Error(`Unsupported setup agent: ${name}`);
149
192
  }
150
- targets.push(...await setupAgentSkillTargets(name, home, env, { includeFallback: requested.length > 0 }));
193
+ targets.push(...await setupAgentSkillTargets(name, home, env, {
194
+ includeFallback: requested.length > 0,
195
+ registeredRoots
196
+ }));
151
197
  }
152
198
  return targets;
153
199
  }
154
200
 
201
+ async function setupSkillRoot(options, home, cwd) {
202
+ const skillRoot = options.get("skill-root");
203
+ if (skillRoot === undefined) return undefined;
204
+ const requested = readCsv(options.get("agent"));
205
+ if (requested.length !== 1) {
206
+ throw new Error("--skill-root requires exactly one --agent value.");
207
+ }
208
+ return await proposedAgentRoot(home, requested[0], skillRoot, cwd);
209
+ }
210
+
211
+ function assertSetupOptions(options) {
212
+ const allowed = new Set(["yes", "agent", "skill-root"]);
213
+ for (const option of options.keys()) {
214
+ if (!allowed.has(option)) throw new Error(`Unknown setup option: --${option}`);
215
+ }
216
+ }
217
+
218
+ function formatSharedEntry(entry) {
219
+ return `${entry.agent}:${entry.skill}:${entry.path}`;
220
+ }
221
+
222
+ function formatBlockedEntry(entry) {
223
+ return `${entry.agent ?? "unknown"}:${entry.skill}:${entry.reason}`;
224
+ }
225
+
155
226
  function commandPrefix(runtime) {
156
227
  const entrypoint = runtime.entrypointPath ?? "";
157
228
  const normalized = entrypoint.replace(/\\/g, "/");
158
229
  if (normalized.includes("/_npx/")) {
159
230
  const packageSpec = runtime.packageSpec ?? "agent-skillboard";
160
- return `npx --yes --package ${shellQuote(packageSpec)} skillboard`;
231
+ return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
161
232
  }
162
233
  if (isSourceTreeEntrypoint(entrypoint)) {
163
- return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd())}`;
234
+ return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
164
235
  }
165
236
  return "skillboard";
166
237
  }
@@ -176,18 +247,21 @@ function isSourceTreeEntrypoint(entrypoint) {
176
247
  && !normalized.includes("/.npm/");
177
248
  }
178
249
 
179
- function sourceTreeEntrypoint(entrypoint, cwd) {
250
+ function sourceTreeEntrypoint(entrypoint, cwd, platform) {
180
251
  const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
181
252
  const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
182
253
  if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
183
- return shellQuote(relativeEntrypoint);
254
+ return shellQuote(relativeEntrypoint, platform);
184
255
  }
185
- return shellQuote(absoluteEntrypoint);
256
+ return shellQuote(absoluteEntrypoint, platform);
186
257
  }
187
258
 
188
- function shellQuote(value) {
259
+ function shellQuote(value, platform = process.platform) {
189
260
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
190
261
  return value;
191
262
  }
263
+ if (platform === "win32") {
264
+ return `"${value.replace(/"/g, '""')}"`;
265
+ }
192
266
  return `'${value.replace(/'/g, "'\\''")}'`;
193
267
  }
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, chown, lstat, readFile } from "node:fs/promises";
2
+ import { access, chown, lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
@@ -53,6 +53,19 @@ export async function applyOwnership(path, ownership) {
53
53
  }
54
54
  }
55
55
 
56
+ export async function applyOwnershipTree(path, ownership) {
57
+ if (ownership === null) return;
58
+ const stats = await pathStats(path);
59
+ if (stats === null || stats.isSymbolicLink()) return;
60
+ await applyOwnership(path, ownership);
61
+ if (!stats.isDirectory()) return;
62
+ const entries = await readdir(path, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ if (entry.isSymbolicLink()) continue;
65
+ await applyOwnershipTree(resolve(path, entry.name), ownership);
66
+ }
67
+ }
68
+
56
69
  function canApplyProcessOwnership(uid, gid) {
57
70
  if (typeof process.getuid !== "function") {
58
71
  return false;
@@ -1,19 +1,22 @@
1
1
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
2
- import { agentSkillRootCandidates } from "./agent-skill-roots.mjs";
2
+ import { activeAgentSkillRootCandidates } from "./agent-skill-roots.mjs";
3
3
 
4
- export async function defaultScanRoots(home, env) {
4
+ export async function defaultScanRoots(home, env, options = {}) {
5
5
  const codexHome = env.CODEX_HOME ?? join(home, ".codex");
6
6
  const agentRoots = await Promise.all([
7
- agentSkillRootCandidates("codex", home, env),
8
- agentSkillRootCandidates("claude", home, env),
9
- agentSkillRootCandidates("opencode", home, env),
10
- agentSkillRootCandidates("hermes", home, env)
7
+ activeAgentSkillRootCandidates("codex", home, env, options),
8
+ activeAgentSkillRootCandidates("claude", home, env, options),
9
+ activeAgentSkillRootCandidates("opencode", home, env, options),
10
+ activeAgentSkillRootCandidates("hermes", home, env, options)
11
11
  ]);
12
12
  return [
13
- join(codexHome, "skills", ".system"),
14
- join(codexHome, "plugins", "cache"),
15
- join(home, ".agents", "shared-skills"),
16
- ...agentRoots.flat().map((root) => root.skillRoot)
13
+ { path: join(codexHome, "skills", ".system") },
14
+ { path: join(codexHome, "plugins", "cache") },
15
+ { path: join(home, ".agents", "shared-skills") },
16
+ ...agentRoots.flatMap((roots, index) => roots.map((root) => ({
17
+ path: root.skillRoot,
18
+ agent: ["codex", "claude", "opencode", "hermes"][index]
19
+ })))
17
20
  ];
18
21
  }
19
22
 
@@ -110,16 +110,16 @@ export async function discoverAgentSkillInventory(options = {}) {
110
110
  const env = options.env ?? process.env;
111
111
  const home = options.home ?? env.HOME ?? env.USERPROFILE ?? homedir();
112
112
  const detectors = options.detectors ?? agentInventoryDetectors;
113
- const roots = uniquePaths([
114
- ...(await defaultScanRoots(home, env)),
115
- ...readCsv(env.SKILLBOARD_INIT_SCAN_ROOTS),
116
- ...(options.roots ?? [])
113
+ const roots = uniqueRootEntries([
114
+ ...(await defaultScanRoots(home, env, { registeredRoots: options.registeredRoots })),
115
+ ...readCsv(env.SKILLBOARD_INIT_SCAN_ROOTS).map((path) => ({ path })),
116
+ ...(options.roots ?? []).map((path) => ({ path }))
117
117
  ], home);
118
118
  const groups = [];
119
119
  const warnings = [];
120
120
 
121
121
  for (const root of roots) {
122
- const discovered = await discoverRoot(root, home, detectors);
122
+ const discovered = await discoverRoot(root.path, home, detectors, root.agent);
123
123
  groups.push(...discovered.groups);
124
124
  warnings.push(...discovered.warnings);
125
125
  }
@@ -293,13 +293,13 @@ function ensureLocalWorkflow(workflowsMap, target, skills, document) {
293
293
  return true;
294
294
  }
295
295
 
296
- async function discoverRoot(root, home, detectors) {
296
+ async function discoverRoot(root, home, detectors, agent) {
297
297
  const path = resolvePath(root, home);
298
298
  if (!(await exists(path))) {
299
299
  return { groups: [], warnings: [] };
300
300
  }
301
301
  const warnings = [];
302
- const detector = detectors.find((candidate) => {
302
+ const matched = detectors.find((candidate) => {
303
303
  try {
304
304
  return candidate.matches(path);
305
305
  } catch (error) {
@@ -307,6 +307,7 @@ async function discoverRoot(root, home, detectors) {
307
307
  return false;
308
308
  }
309
309
  });
310
+ const detector = agent === undefined ? matched : forcedAgentDetector(agent, matched, detectors);
310
311
  if (detector === undefined) {
311
312
  return { groups: [], warnings };
312
313
  }
@@ -319,6 +320,12 @@ async function discoverRoot(root, home, detectors) {
319
320
  }
320
321
  }
321
322
 
323
+ function forcedAgentDetector(agent, matched, detectors) {
324
+ if (matched?.id === "hermes-profile-skills" && agent === "hermes") return matched;
325
+ const id = `${agent}-user-skills`;
326
+ return detectors.find((candidate) => candidate.id === id) ?? matched;
327
+ }
328
+
322
329
  async function discoverSkillDirectory(root, unit, options) {
323
330
  const files = await findSkillFiles(root, root, options);
324
331
  return files.length === 0 ? [] : [{ unit, root, files }];
@@ -898,21 +905,20 @@ function uniqueStrings(values) {
898
905
  return [...new Set(values.filter((value) => value.trim().length > 0))];
899
906
  }
900
907
 
901
- function uniquePaths(values, home) {
902
- const seen = new Set();
903
- const result = [];
908
+ function uniqueRootEntries(values, home) {
909
+ const byPath = new Map();
904
910
  for (const value of values) {
905
- const trimmed = value.trim();
911
+ const trimmed = value.path.trim();
906
912
  if (trimmed.length === 0) {
907
913
  continue;
908
914
  }
909
915
  const key = resolve(trimmed.startsWith("~/") ? join(home, trimmed.slice(2)) : trimmed);
910
- if (!seen.has(key)) {
911
- seen.add(key);
912
- result.push(trimmed);
916
+ const existing = byPath.get(key);
917
+ if (existing === undefined || existing.agent === undefined && value.agent !== undefined) {
918
+ byPath.set(key, { path: trimmed, ...(value.agent === undefined ? {} : { agent: value.agent }) });
913
919
  }
914
920
  }
915
- return result;
921
+ return [...byPath.values()];
916
922
  }
917
923
 
918
924
  function errorMessage(error) {