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 +42 -0
- package/README.md +37 -2
- package/bin/postinstall.mjs +2 -1
- package/docs/ai-skill-routing-goal.md +7 -0
- package/docs/install.md +48 -2
- package/docs/reference.md +20 -1
- package/docs/user-flow.md +13 -0
- package/docs/versioning.md +16 -3
- package/package.json +1 -1
- package/src/agent-integration-cli.mjs +87 -13
- package/src/agent-integration-home.mjs +14 -1
- package/src/agent-inventory-platforms.mjs +13 -10
- package/src/agent-inventory.mjs +21 -15
- package/src/agent-root-registry.mjs +127 -0
- package/src/agent-skill-roots.mjs +70 -13
- package/src/cli.mjs +24 -4
- package/src/doctor.mjs +8 -1
- package/src/install-health.mjs +177 -0
- package/src/inventory-refresh.mjs +6 -2
- package/src/shared-skill-reconcile.mjs +97 -0
- package/src/shared-skill.mjs +40 -16
- package/src/user-uninstall.mjs +18 -3
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.
|
|
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
|
-
|
|
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
|
|
package/bin/postinstall.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
16
|
-
|
|
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:
|
package/docs/versioning.md
CHANGED
|
@@ -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.
|
|
38
|
-
|
|
39
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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, {
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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
|
|
package/src/agent-inventory.mjs
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
902
|
-
const
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
921
|
+
return [...byPath.values()];
|
|
916
922
|
}
|
|
917
923
|
|
|
918
924
|
function errorMessage(error) {
|