agent-skillboard 0.3.2 → 0.3.4
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 +31 -0
- package/README.md +18 -7
- package/docs/ai-skill-routing-goal.md +14 -7
- package/docs/policy-model.md +14 -3
- package/docs/positioning.md +4 -2
- package/docs/reference.md +24 -7
- package/docs/routing.md +19 -8
- package/docs/user-flow.md +9 -5
- package/docs/value-proof.md +5 -1
- package/docs/variant-lifecycle.md +2 -1
- package/package.json +1 -1
- package/src/advisor/guidance.mjs +20 -2
- package/src/advisor/schema.mjs +2 -1
- package/src/agent-integration-content.mjs +6 -6
- package/src/brief-renderer.mjs +15 -1
- package/src/cli.mjs +74 -43
- package/src/doctor.mjs +13 -14
- package/src/lifecycle-cli.mjs +1 -1
- package/src/lifecycle-content.mjs +8 -6
- package/src/route-advisory.mjs +0 -44
- package/src/route-renderer.mjs +9 -2
- package/src/route-selection.mjs +11 -33
- package/src/route-tokens.mjs +20 -2
- package/src/route.mjs +35 -52
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.4 — 2026-07-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Separated generated-inventory integrity failures from sorted stale
|
|
10
|
+
removed-skill policy observations. Valid inventory now keeps doctor, status,
|
|
11
|
+
brief, strict health, and observed-skill routing healthy while absent skills
|
|
12
|
+
remain guard-denied and available for explicit, confirmation-required policy
|
|
13
|
+
forget actions.
|
|
14
|
+
|
|
15
|
+
## 0.3.3 — 2026-07-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added deterministic Unicode intent tokenization with whole-run and character
|
|
20
|
+
bigram matching for legacy v1 compatibility routing.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Removed v2 request tokenization, metadata scoring, ranked hints, and
|
|
25
|
+
deterministic recommendations. Route and brief now return raw eligible skill
|
|
26
|
+
descriptions and saved preferences for the active model to interpret.
|
|
27
|
+
- Made route, can-use, and guard errors explain whether the detected policy
|
|
28
|
+
expects v2 `--agent` or v1 `--workflow`, including the v1 migration preview.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Preserved legacy v1 explicit skill-id selection when an ASCII id is
|
|
33
|
+
immediately followed by a Hangul or CJK particle, without allowing Latin
|
|
34
|
+
prefix matches.
|
|
35
|
+
|
|
5
36
|
## 0.3.2 — 2026-07-14
|
|
6
37
|
|
|
7
38
|
### Changed
|
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ SkillBoard gives AI agents one small user-level policy for installed skills:
|
|
|
6
6
|
2. Keep an enabled skill agent-local or shared across agents.
|
|
7
7
|
|
|
8
8
|
Valid installed skills default to enabled and agent-local. Sharing is opt-in per
|
|
9
|
-
skill.
|
|
10
|
-
and
|
|
9
|
+
skill. The model makes semantic skill choices from the request, eligible skill
|
|
10
|
+
descriptions, and raw saved preferences. SkillBoard does not tokenize, score,
|
|
11
|
+
match, or recommend from v2 request text.
|
|
11
12
|
|
|
12
|
-
Status: public alpha. Package 0.3.
|
|
13
|
+
Status: public alpha. Package 0.3.4 writes policy schema v2.
|
|
13
14
|
|
|
14
15
|
<p align="center">
|
|
15
16
|
<img src="https://raw.githubusercontent.com/NyXXiR/skillboard/main/skillboard.png" alt="SkillBoard architecture diagram" width="100%">
|
|
@@ -128,8 +129,9 @@ destructive actions, and secrets.
|
|
|
128
129
|
From any working directory, an agent can:
|
|
129
130
|
|
|
130
131
|
1. Read `skillboard brief --intent <request> --agent <agent> --json`.
|
|
131
|
-
2. Select
|
|
132
|
-
|
|
132
|
+
2. Select the best eligible skill from the full request, descriptions, local
|
|
133
|
+
instructions, and raw saved preferences, or use no skill. The model itself
|
|
134
|
+
interprets explicit user direction and preference semantics.
|
|
133
135
|
3. Run `skillboard guard use <skill-id> --agent <agent> --json` before use.
|
|
134
136
|
4. Work without another approval when the guard allows use.
|
|
135
137
|
5. Ask after completion only if remembering a preference would help.
|
|
@@ -151,8 +153,8 @@ current action, runs `skillboard apply-action <action-id> --agent <agent> --yes
|
|
|
151
153
|
rereads the returned post-apply brief.
|
|
152
154
|
|
|
153
155
|
This is the distinction from a pure distribution tool: skills stay local by
|
|
154
|
-
default, users promote only selected skills to shared use, and
|
|
155
|
-
can be remembered without changing installation or availability.
|
|
156
|
+
default, users promote only selected skills to shared use, and a model-facing
|
|
157
|
+
preference can be remembered without changing installation or availability.
|
|
156
158
|
|
|
157
159
|
## Migrate version 1
|
|
158
160
|
|
|
@@ -190,6 +192,15 @@ skillboard skill forget <skill-id>
|
|
|
190
192
|
`forget` never deletes skill files. It refuses skills that are still observed or
|
|
191
193
|
shared, so shared skills must be unshared first.
|
|
192
194
|
|
|
195
|
+
A valid generated inventory remains healthy when it no longer observes an
|
|
196
|
+
unshared policy entry. Doctor/status expose sorted entries as
|
|
197
|
+
`inventory.stalePolicySkills`; brief exposes the same observation as
|
|
198
|
+
`health.inventory.stale_policy_skills`. Stale removed-skill policy does not fail
|
|
199
|
+
strict health, change passed mode, or block other observed skills. The absent
|
|
200
|
+
skill itself remains unavailable to route and guard. SkillBoard never deletes
|
|
201
|
+
these entries automatically: review one current forget action, confirm it, and
|
|
202
|
+
then reread the returned brief.
|
|
203
|
+
|
|
193
204
|
Before removing the SkillBoard package, preview and apply the complete
|
|
194
205
|
user-level cleanup:
|
|
195
206
|
|
|
@@ -7,16 +7,23 @@ cross-agent sharing, and overlap routing. It asks only two policy questions:
|
|
|
7
7
|
2. Should SkillBoard share this skill with other supported agents?
|
|
8
8
|
|
|
9
9
|
Installed, valid skills default to enabled and agent-local. A user may disable
|
|
10
|
-
a skill or opt that individual skill into sharing. An optional preference
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
a skill or opt that individual skill into sharing. An optional preference is
|
|
11
|
+
returned as raw context beside eligible skills; it never changes availability
|
|
12
|
+
or copies files.
|
|
13
|
+
|
|
14
|
+
The active model owns all v2 semantic skill selection. SkillBoard returns the
|
|
15
|
+
request unchanged with enabled skills installed for the current agent, their
|
|
16
|
+
descriptions, and raw saved preferences. It does not tokenize, normalize,
|
|
17
|
+
score, match, or recommend from v2 request text. The model interprets Korean,
|
|
18
|
+
explicit user direction, and preferences directly, and may choose no skill.
|
|
13
19
|
|
|
14
20
|
## Product loop
|
|
15
21
|
|
|
16
22
|
Preserve `observe → route → work → explain briefly → ask after → remember`.
|
|
17
23
|
Ordinary work must not stop for source review or pre-task configuration. Read
|
|
18
|
-
the current brief when skills overlap,
|
|
19
|
-
once only before a persistent policy
|
|
24
|
+
the current brief when skills overlap, make the semantic choice in the active
|
|
25
|
+
model, run the guard before use, and ask once only before a persistent policy
|
|
26
|
+
change.
|
|
20
27
|
|
|
21
28
|
## Boundaries
|
|
22
29
|
|
|
@@ -55,8 +62,8 @@ unknown future ambiguity unchanged for an explicit decision.
|
|
|
55
62
|
Every route, brief, guard, dashboard, generated bridge, and policy mutation must
|
|
56
63
|
project the same v2 meaning. Normal commands use one home control plane from any
|
|
57
64
|
working directory; project initialization is not part of the v2 flow. Never
|
|
58
|
-
reintroduce source reputation, workflow scope,
|
|
59
|
-
authorization checks.
|
|
65
|
+
reintroduce source reputation, workflow scope, v2 tokenizers or metadata scores,
|
|
66
|
+
or legacy state fields as hidden authorization or semantic-selection checks.
|
|
60
67
|
|
|
61
68
|
Version 1 is migration input only. It has a one-release read-only window in the
|
|
62
69
|
v0.3 release and is removed in v0.4.0.
|
package/docs/policy-model.md
CHANGED
|
@@ -24,7 +24,7 @@ compatible copies for other supported agents.
|
|
|
24
24
|
|
|
25
25
|
## Optional routing preference
|
|
26
26
|
|
|
27
|
-
Preference
|
|
27
|
+
Preference is raw context shown to the model for an eligible skill. It never
|
|
28
28
|
changes availability and never shares files.
|
|
29
29
|
|
|
30
30
|
```yaml
|
|
@@ -38,8 +38,10 @@ changes availability and never shares files.
|
|
|
38
38
|
priority: 100
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
The model interprets explicit user selection and saved preference semantics from
|
|
42
|
+
the full request. SkillBoard does not match the `intents` strings or use
|
|
43
|
+
`priority` as a score in v2. A high priority cannot enable a disabled skill or
|
|
44
|
+
make a skill appear on an agent where it is absent.
|
|
43
45
|
|
|
44
46
|
## Generated inventory
|
|
45
47
|
|
|
@@ -52,6 +54,15 @@ optional audit metadata and never determine availability. Runtime and action
|
|
|
52
54
|
authorization are outside SkillBoard's scope and remain with the agent or
|
|
53
55
|
harness.
|
|
54
56
|
|
|
57
|
+
Generated-inventory integrity is separate from stale removed-skill policy.
|
|
58
|
+
Missing, malformed, unsupported, duplicate, or internally inconsistent
|
|
59
|
+
inventory remains a hard health failure. When inventory is valid, policy ids it
|
|
60
|
+
does not observe are reported as sorted stale-policy observations without
|
|
61
|
+
failing strict health, changing passed mode, or blocking other observed skills.
|
|
62
|
+
The absent skill remains unavailable to route and guard. Stale policy is retained
|
|
63
|
+
until an explicit, confirmation-required forget action removes only that policy
|
|
64
|
+
entry; SkillBoard never auto-deletes it.
|
|
65
|
+
|
|
55
66
|
## Installation default
|
|
56
67
|
|
|
57
68
|
A newly discovered valid skill receives `enabled: true` and `shared: false`.
|
package/docs/positioning.md
CHANGED
|
@@ -8,8 +8,10 @@ Its durable policy answers two questions:
|
|
|
8
8
|
1. Is this skill enabled?
|
|
9
9
|
2. Should this one skill remain agent-local or be shared across agents?
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
The active model chooses the best enabled skill installed for the current
|
|
12
|
+
agent, or no skill, from raw descriptions and optional saved preferences.
|
|
13
|
+
SkillBoard does not tokenize, score, or match v2 request text. Preference never
|
|
14
|
+
changes availability.
|
|
13
15
|
|
|
14
16
|
## Difference from distribution tools
|
|
15
17
|
|
package/docs/reference.md
CHANGED
|
@@ -16,9 +16,9 @@ Source tree: replace `skillboard ` with `node bin/skillboard.mjs `.
|
|
|
16
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
|
-
skillboard route <intent> --agent
|
|
20
|
-
skillboard can-use <skill-id> --agent
|
|
21
|
-
skillboard guard use <skill-id> --agent
|
|
19
|
+
skillboard route <intent> --agent <name> (v2 policy) | --workflow <name> (v1 policy) [--json]
|
|
20
|
+
skillboard can-use <skill-id> --agent <name> (v2 policy) | --workflow <name> (v1 policy) [--json]
|
|
21
|
+
skillboard guard use <skill-id> --agent <name> (v2 policy) | --workflow <name> (v1 policy) [--json]
|
|
22
22
|
skillboard skill enable <skill-id> [--dry-run] [--json]
|
|
23
23
|
skillboard skill disable <skill-id> [--dry-run] [--json]
|
|
24
24
|
skillboard skill share <skill-id> [--dry-run] [--json]
|
|
@@ -50,12 +50,18 @@ non-symlinked path inside the invoking user's home, and persists the mapping in
|
|
|
50
50
|
not authorization or policy. One root cannot be registered to two agents.
|
|
51
51
|
|
|
52
52
|
Guard requires a valid inventory record, `enabled: true`, and the selected agent
|
|
53
|
-
in `installed_on`.
|
|
54
|
-
|
|
53
|
+
in `installed_on`. For v2 routing, SkillBoard returns the request plus raw
|
|
54
|
+
eligible skill descriptions and saved preferences. It does not tokenize, score,
|
|
55
|
+
match, or recommend from request text. The active model interprets explicit
|
|
56
|
+
direction and preferences, chooses a skill or no skill, then runs guard.
|
|
57
|
+
Preference never changes availability.
|
|
55
58
|
|
|
56
59
|
Version 2 route, can-use, and guard require `--agent`; generated agent guidance
|
|
57
60
|
passes it automatically. This prevents one agent from selecting another
|
|
58
61
|
agent's local-only skill without adding a user prompt to the normal flow.
|
|
62
|
+
Version 1 compatibility policy uses `--workflow` instead. Passing the selector
|
|
63
|
+
for the other policy version exits with guidance for the detected version and,
|
|
64
|
+
for v1, the exact `migrate v2` preview command.
|
|
59
65
|
|
|
60
66
|
## Config schema v2
|
|
61
67
|
|
|
@@ -79,8 +85,8 @@ skills:
|
|
|
79
85
|
- `enabled` and `shared` are required Booleans.
|
|
80
86
|
- `shared: false` means agent-local; `shared: true` enables managed cross-agent
|
|
81
87
|
copies while preserving the original.
|
|
82
|
-
- `preference` is optional
|
|
83
|
-
agent
|
|
88
|
+
- `preference` is optional raw context for model selection when the skill is
|
|
89
|
+
enabled and installed for the current agent. SkillBoard does not score it.
|
|
84
90
|
- Unknown policy keys are rejected.
|
|
85
91
|
- Skill ids use letters, numbers, `.`, `_`, `:`, and `-`; path-like ids are rejected.
|
|
86
92
|
- A valid newly discovered skill defaults to enabled and agent-local.
|
|
@@ -97,6 +103,17 @@ provenance, digests, aliases, install units, and runtime-component observations.
|
|
|
97
103
|
Only `installed_on` participates in the presence check. Other observations are
|
|
98
104
|
optional audit metadata and never determine availability.
|
|
99
105
|
|
|
106
|
+
Missing, invalid JSON, unsupported flags or format, duplicate records, and
|
|
107
|
+
malformed records are generated-inventory integrity failures. With an otherwise
|
|
108
|
+
valid inventory, policy ids absent from inventory are instead sorted,
|
|
109
|
+
non-blocking stale-policy observations. Doctor/status return them in
|
|
110
|
+
`inventory.stalePolicySkills`; brief returns them in
|
|
111
|
+
`health.inventory.stale_policy_skills`. They do not fail strict health or stop
|
|
112
|
+
observed skills from routing and guard, while the specifically absent skill
|
|
113
|
+
remains unavailable. No command automatically removes stale policy. A current
|
|
114
|
+
`v2:forget-skill:<skill-id>` action requires explicit confirmation, refuses
|
|
115
|
+
installed or shared skills, and changes policy only.
|
|
116
|
+
|
|
100
117
|
Runtime and action authorization are outside SkillBoard's scope. The active
|
|
101
118
|
agent or harness owns permission checks for hooks, MCP servers, commands,
|
|
102
119
|
network access, external writes, destructive operations, and secrets.
|
package/docs/routing.md
CHANGED
|
@@ -4,16 +4,22 @@ Routing chooses among skills already available to the current agent. Guard
|
|
|
4
4
|
decides availability from valid generated inventory, `enabled`, and
|
|
5
5
|
`installed_on`; routing cannot override that verdict.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## v2 resolution order
|
|
8
8
|
|
|
9
9
|
1. Remove skills missing valid inventory records.
|
|
10
10
|
2. Remove disabled skills.
|
|
11
11
|
3. Remove skills not installed for the selected agent.
|
|
12
|
-
4.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
4. Return every remaining skill's id, name, description, path, and raw optional
|
|
13
|
+
preference beside the request.
|
|
14
|
+
5. Let the active model interpret the full request, explicit user direction,
|
|
15
|
+
descriptions, instructions, and preferences and choose a skill or no skill.
|
|
16
|
+
6. Guard the model-selected skill immediately before use.
|
|
17
|
+
|
|
18
|
+
V2 has no intent tokenizer, metadata score, request matcher, ranked candidates,
|
|
19
|
+
or deterministic recommendation. Korean and other languages are handled by the
|
|
20
|
+
foundation model. Preference is raw model context and never changes
|
|
21
|
+
availability. Version 1 compatibility routing retains its deterministic
|
|
22
|
+
tokenizer until the v1 reader is removed in v0.4.0.
|
|
17
23
|
|
|
18
24
|
```bash
|
|
19
25
|
skillboard brief --intent "write tests" --agent codex --json
|
|
@@ -26,8 +32,13 @@ These commands use `~/skillboard.config.yaml` and
|
|
|
26
32
|
`--config` and `--skills` overrides remain available for migration, testing,
|
|
27
33
|
and legacy workspaces.
|
|
28
34
|
|
|
29
|
-
When guard allows use, continue without another approval.
|
|
30
|
-
|
|
35
|
+
When guard allows use, continue without another approval. If the model-selected
|
|
36
|
+
choice was ambiguous, finish first and ask afterward only when remembering a
|
|
37
|
+
preference would reduce future ambiguity.
|
|
38
|
+
|
|
39
|
+
Version 1 compatibility routing remains workflow-driven and may return a
|
|
40
|
+
deterministic capability route. It asks for clarification when no workflow
|
|
41
|
+
route matches.
|
|
31
42
|
|
|
32
43
|
Source and provenance findings are optional audit metadata and never determine
|
|
33
44
|
availability. Runtime and action authorization are outside SkillBoard's scope.
|
package/docs/user-flow.md
CHANGED
|
@@ -41,8 +41,11 @@ skillboard guard use <skill-id> --agent codex --json
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The guard allows an enabled skill only when generated inventory records it on
|
|
44
|
-
the selected agent.
|
|
45
|
-
|
|
44
|
+
the selected agent. SkillBoard returns raw eligible descriptions and saved
|
|
45
|
+
preferences without tokenizing, scoring, or matching the request. The model
|
|
46
|
+
interprets the full request, explicit direction, and preferences and chooses a
|
|
47
|
+
skill or no skill. Preference never changes availability. Allowed use does not
|
|
48
|
+
require another confirmation.
|
|
46
49
|
|
|
47
50
|
## 3. Change one decision
|
|
48
51
|
|
|
@@ -65,9 +68,10 @@ returned post-apply brief. Cached action ids are not reused.
|
|
|
65
68
|
|
|
66
69
|
## 4. Ask after, then remember
|
|
67
70
|
|
|
68
|
-
When a route was ambiguous, the agent finishes first. It may then
|
|
69
|
-
remember an intent preference. Preference
|
|
70
|
-
share, enable, disable, install, or
|
|
71
|
+
When a model-selected route was ambiguous, the agent finishes first. It may then
|
|
72
|
+
ask whether to remember an intent preference. Preference becomes raw context
|
|
73
|
+
for later model selection; it does not share, enable, disable, install, or
|
|
74
|
+
remove a skill.
|
|
71
75
|
|
|
72
76
|
## 5. Audit separately
|
|
73
77
|
|
package/docs/value-proof.md
CHANGED
|
@@ -12,7 +12,11 @@ node --test test/v2-agent-sharing.test.mjs test/v2-onboarding-behavior.test.mjs
|
|
|
12
12
|
- Valid discovered skills default to enabled and agent-local.
|
|
13
13
|
- Disabled skills and skills absent from the selected agent are denied.
|
|
14
14
|
- Share and unshare preserve agent-owned originals.
|
|
15
|
-
-
|
|
15
|
+
- V2 returns raw eligible descriptions and preferences without interpreting
|
|
16
|
+
request text or producing a recommendation.
|
|
17
|
+
- Korean, explicit skill direction, and saved preference semantics remain owned
|
|
18
|
+
by the active model.
|
|
19
|
+
- Optional preference never changes availability.
|
|
16
20
|
- Source and provenance audits do not change guard results.
|
|
17
21
|
- Commands produce the same result from different working directories.
|
|
18
22
|
- Version 1 project policy remains read-only until explicit migration; setup
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
A variant is a manual content adaptation with snapshot and digest history. The
|
|
4
4
|
lifecycle does not authorize availability. A registered variant is usable only
|
|
5
5
|
according to its ordinary v2 `enabled` entry and generated installation
|
|
6
|
-
presence; optional preference
|
|
6
|
+
presence; optional preference is raw model context and never changes
|
|
7
|
+
availability.
|
|
7
8
|
|
|
8
9
|
## Current v2 boundary
|
|
9
10
|
|
package/package.json
CHANGED
package/src/advisor/guidance.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { command } from "./action-core.mjs";
|
|
|
3
3
|
const GUARD_WHEN = "immediately before skill use";
|
|
4
4
|
const GOAL_DOCUMENT = Object.freeze({
|
|
5
5
|
path: "docs/ai-skill-routing-goal.md",
|
|
6
|
-
purpose: "Preserve SkillBoard v2 as a user-level control plane: enable or disable a skill, opt individual skills into cross-agent sharing, and
|
|
6
|
+
purpose: "Preserve SkillBoard v2 as a user-level control plane: enable or disable a skill, opt individual skills into cross-agent sharing, and expose optional preference as raw context for the active model.",
|
|
7
7
|
loop: Object.freeze([
|
|
8
8
|
"observe",
|
|
9
9
|
"route",
|
|
@@ -140,14 +140,22 @@ function recommendedNextStep(status, brief, choices, route = null) {
|
|
|
140
140
|
switch (status) {
|
|
141
141
|
case "ready":
|
|
142
142
|
if (route !== null) {
|
|
143
|
+
if (route.model_selection_required === true) {
|
|
144
|
+
return modelSelectionStep(route);
|
|
145
|
+
}
|
|
143
146
|
return route.recommended_skill === null
|
|
144
|
-
?
|
|
147
|
+
? route.workflow === null
|
|
148
|
+
? "No enabled skill is available for this request."
|
|
149
|
+
: "Ask a clarifying question; no workflow capability matched this request."
|
|
145
150
|
: `Use ${route.recommended_skill} for this request after the guard check passes${postUsePolicyStep(route)}.`;
|
|
146
151
|
}
|
|
147
152
|
return availableSkillCount(brief) === 0
|
|
148
153
|
? "No policy change is required; valid installed skills default to enabled and agent-local."
|
|
149
154
|
: "Run the guard check immediately before using any selected skill.";
|
|
150
155
|
case "needs-decision":
|
|
156
|
+
if (route?.model_selection_required === true) {
|
|
157
|
+
return `${modelSelectionStep(route)} Handle pending review decisions after the task unless a policy-changing action is needed now.`;
|
|
158
|
+
}
|
|
151
159
|
if (route?.recommended_skill !== null && route?.guard_allowed === true) {
|
|
152
160
|
return route.post_use_policy_suggestion === null
|
|
153
161
|
? `Use ${route.recommended_skill} for this request after the guard check passes; handle pending review decisions after the task unless a policy-changing action is needed now.`
|
|
@@ -180,6 +188,10 @@ function recommendedNextStep(status, brief, choices, route = null) {
|
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
function modelSelectionStep(route) {
|
|
192
|
+
return "Choose the best enabled, installed skill from the request's semantic context, skill descriptions, and raw saved preferences. Run its guard before use, or continue without a skill when none fits.";
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
function postUsePolicyStep(route) {
|
|
184
196
|
return route.post_use_policy_suggestion === null
|
|
185
197
|
? ""
|
|
@@ -197,6 +209,8 @@ function routeGuidance(route) {
|
|
|
197
209
|
matched_terms: route.matched_terms,
|
|
198
210
|
recommendation_reason: route.recommendation_reason,
|
|
199
211
|
recommended_skill: route.recommended_skill,
|
|
212
|
+
selection_mode: route.selection_mode ?? null,
|
|
213
|
+
model_selection_required: route.model_selection_required ?? false,
|
|
200
214
|
fallback_skills: route.fallback_skills,
|
|
201
215
|
route_candidates: (route.route_candidates ?? []).map((candidate) => ({
|
|
202
216
|
skill: candidate.skill,
|
|
@@ -216,6 +230,10 @@ function routeGuidance(route) {
|
|
|
216
230
|
guard_reasons: route.guard?.reasons ?? [],
|
|
217
231
|
possible_skills: route.possible_skills.map((skill) => ({
|
|
218
232
|
id: skill.id,
|
|
233
|
+
name: skill.name,
|
|
234
|
+
description: skill.description,
|
|
235
|
+
path: skill.path,
|
|
236
|
+
preference: skill.preference,
|
|
219
237
|
category: skill.category,
|
|
220
238
|
allowed: skill.allowed
|
|
221
239
|
}))
|
package/src/advisor/schema.mjs
CHANGED
|
@@ -93,7 +93,8 @@ export function healthFromDoctor(doctor, paths) {
|
|
|
93
93
|
required: true,
|
|
94
94
|
ok: doctor.inventory.ok,
|
|
95
95
|
path: doctor.inventory.path,
|
|
96
|
-
errors: sortedStrings(doctor.inventory.errors)
|
|
96
|
+
errors: sortedStrings(doctor.inventory.errors),
|
|
97
|
+
stale_policy_skills: sortedStrings(doctor.inventory.stalePolicySkills ?? [])
|
|
97
98
|
};
|
|
98
99
|
}
|
|
99
100
|
return health;
|
|
@@ -25,7 +25,7 @@ Use this skill to let SkillBoard guide skill selection above individual projects
|
|
|
25
25
|
- For ordinary user requests, work normally; invoke SkillBoard only when skill choice is ambiguous, skills overlap, or the user asks for a SkillBoard or skill decision.
|
|
26
26
|
- If the user explicitly asks for a specific installed skill, honor that request when the guard allows it instead of rerouting away solely because other skills also match.
|
|
27
27
|
- Do not ask for permission merely because you selected a skill.
|
|
28
|
-
- When you use a skill, disclose it briefly at the start and completion. If
|
|
28
|
+
- When you use a skill, disclose it briefly at the start and completion. If a saved preference influenced the model's choice, mention that briefly at completion.
|
|
29
29
|
|
|
30
30
|
## Cross-Agent Skill Reuse
|
|
31
31
|
|
|
@@ -41,11 +41,11 @@ Use this skill to let SkillBoard guide skill selection above individual projects
|
|
|
41
41
|
|
|
42
42
|
1. Start from the user's task, not from a pre-task inventory prompt.
|
|
43
43
|
2. Identify candidate skills only when multiple installed skills plausibly match or a manual skill-control request is present.
|
|
44
|
-
3.
|
|
45
|
-
4.
|
|
46
|
-
5. Run \`skillboard guard use <skill-id> --agent ${agent} --json\` immediately before invoking the selected skill.
|
|
47
|
-
6.
|
|
48
|
-
7.
|
|
44
|
+
3. Use \`skillboard brief --agent ${agent} --intent <request> --json\` or \`skillboard route <intent> --agent ${agent} --json\` to read eligible skill descriptions and raw saved preferences. SkillBoard does not interpret v2 request text.
|
|
45
|
+
4. Make the semantic choice yourself from the full request, explicit user direction, descriptions, local instructions, and any saved preferences; use no skill when none fits.
|
|
46
|
+
5. Run \`skillboard guard use <skill-id> --agent ${agent} --json\` immediately before invoking the model-selected skill.
|
|
47
|
+
6. Continue with the selected skill; do not stop only because other candidate skills exist.
|
|
48
|
+
7. If the choice remained ambiguous and remembering it would help, finish the work first, then ask whether to save that intent preference.
|
|
49
49
|
|
|
50
50
|
## Removal
|
|
51
51
|
|
package/src/brief-renderer.mjs
CHANGED
|
@@ -53,6 +53,7 @@ export function renderSkillBrief(brief, options = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
emitIntentRoute(lines, brief);
|
|
55
55
|
emitPolicyHealth(lines, brief);
|
|
56
|
+
emitStalePolicy(lines, brief);
|
|
56
57
|
emitCategorySummary(lines, brief);
|
|
57
58
|
emitNextAction(lines, brief, { verbose });
|
|
58
59
|
emitAvailableNowSection(lines, brief, { verbose });
|
|
@@ -84,7 +85,7 @@ function emitIntentRoute(lines, brief) {
|
|
|
84
85
|
if (route === undefined) {
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
|
-
lines.push("##
|
|
88
|
+
lines.push("## Skill selection for this request", "");
|
|
88
89
|
lines.push(...renderRouteSectionLines(route, {
|
|
89
90
|
format: "brief",
|
|
90
91
|
nextStep: brief.assistant_guidance?.recommended_next_step
|
|
@@ -109,6 +110,19 @@ function emitPolicyHealth(lines, brief) {
|
|
|
109
110
|
lines.push("");
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function emitStalePolicy(lines, brief) {
|
|
114
|
+
const skills = brief.health?.inventory?.stale_policy_skills ?? [];
|
|
115
|
+
if (skills.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
lines.push("## Stale removed-skill policy", "");
|
|
119
|
+
lines.push("These policy entries are retained for explicit review and do not block other installed skills.");
|
|
120
|
+
for (const skill of skills) {
|
|
121
|
+
lines.push(`- ${safeText(skill)}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
function emitPolicyDiagnostics(lines, label, diagnostics) {
|
|
113
127
|
if (diagnostics.length === 0) {
|
|
114
128
|
return;
|
package/src/cli.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import { ApplyActionError, applyActionErrorPayload, applyAdvisorAction } from ".
|
|
|
60
60
|
import { importAgentSkill, renderImportAgentSkill } from "./agent-skill-import.mjs";
|
|
61
61
|
import { runBriefCommand } from "./brief-cli.mjs";
|
|
62
62
|
import { renderSkillBrief } from "./brief-renderer.mjs";
|
|
63
|
+
import { shellQuote } from "./agent-integration-command.mjs";
|
|
63
64
|
import { planGuardHookInstall } from "./control.mjs";
|
|
64
65
|
import { writeCheckedConfig } from "./control/config-write.mjs";
|
|
65
66
|
import { runInitCommand, runSetupCommand, runUninstallCommand } from "./lifecycle-cli.mjs";
|
|
@@ -93,7 +94,7 @@ const COMMAND_USAGE = new Map([
|
|
|
93
94
|
["check", ["check --config <path> --skills <dir>"]],
|
|
94
95
|
["list", ["list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]"]],
|
|
95
96
|
["explain", ["explain <skill-id> --config <path> --skills <dir> [--json]"]],
|
|
96
|
-
["can-use", ["can-use <skill-id> --agent
|
|
97
|
+
["can-use", ["can-use <skill-id> --agent <name> (v2 policy) | --workflow <name> (v1 policy) [--json]"]],
|
|
97
98
|
["audit", ["audit sources --config <path> --skills <dir> [--verify] [--json]"]],
|
|
98
99
|
["rollout", ["rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]"]],
|
|
99
100
|
["hook", ["hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]"]],
|
|
@@ -580,15 +581,12 @@ async function route(argv, options, stdout) {
|
|
|
580
581
|
const intent = positionalArgs(argv).join(" ").trim();
|
|
581
582
|
const workflow = options.get("workflow");
|
|
582
583
|
if (intent.length === 0) {
|
|
583
|
-
throw new Error("
|
|
584
|
+
throw new Error(selectorUsage("skillboard route <intent>"));
|
|
584
585
|
}
|
|
585
586
|
const config = configPath(options);
|
|
586
587
|
const skills = skillsRoot(options);
|
|
587
588
|
const workspace = await loadWorkspace({ configPath: config, skillsRoot: skills });
|
|
588
|
-
|
|
589
|
-
if (workspace.version === 1 && workflow === undefined) {
|
|
590
|
-
throw new Error("Usage: skillboard route <intent> --workflow <name>");
|
|
591
|
-
}
|
|
589
|
+
assertPolicySelectorOption(workspace, options, "skillboard route <intent>", config);
|
|
592
590
|
const result = routeSkill(workspace, {
|
|
593
591
|
intent,
|
|
594
592
|
workflow,
|
|
@@ -605,13 +603,11 @@ async function canUse(argv, options, stdout) {
|
|
|
605
603
|
const skillId = positionalArgs(argv)[0];
|
|
606
604
|
const workflow = options.get("workflow");
|
|
607
605
|
if (skillId === undefined) {
|
|
608
|
-
throw new Error("
|
|
609
|
-
}
|
|
610
|
-
const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
|
|
611
|
-
assertV2AgentOption(workspace, options, "skillboard can-use <skill-id> --agent <name>");
|
|
612
|
-
if (workspace.version === 1 && workflow === undefined) {
|
|
613
|
-
throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
|
|
606
|
+
throw new Error(selectorUsage("skillboard can-use <skill-id>"));
|
|
614
607
|
}
|
|
608
|
+
const config = configPath(options);
|
|
609
|
+
const workspace = await loadWorkspace({ configPath: config, skillsRoot: skillsRoot(options) });
|
|
610
|
+
assertPolicySelectorOption(workspace, options, "skillboard can-use <skill-id>", config);
|
|
615
611
|
const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
|
|
616
612
|
writeOutput(stdout, result, options, () => renderCanUse(result));
|
|
617
613
|
return result.allowed ? 0 : 2;
|
|
@@ -623,9 +619,10 @@ async function guard(argv, options, stdout) {
|
|
|
623
619
|
const skillId = args[0] === "use" ? args[1] : args[0];
|
|
624
620
|
const workflow = options.get("workflow");
|
|
625
621
|
if (skillId === undefined) {
|
|
626
|
-
throw new Error("
|
|
622
|
+
throw new Error(selectorUsage("skillboard guard use <skill-id>"));
|
|
627
623
|
}
|
|
628
|
-
const
|
|
624
|
+
const config = configPath(options);
|
|
625
|
+
const workspace = await loadWorkspace({ configPath: config, skillsRoot: skillsRoot(options) });
|
|
629
626
|
const hookProjection = options.get("hook-projection-version") ?? process.env.SKILLBOARD_POLICY_PROJECTION_VERSION;
|
|
630
627
|
if (args[0] !== "use" && hookProjection === undefined) {
|
|
631
628
|
throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
|
|
@@ -634,10 +631,7 @@ async function guard(argv, options, stdout) {
|
|
|
634
631
|
throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
|
|
635
632
|
}
|
|
636
633
|
assertCurrentProjectionVersion(hookProjection, workspace.version);
|
|
637
|
-
|
|
638
|
-
if (workspace.version === 1 && workflow === undefined) {
|
|
639
|
-
throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
|
|
640
|
-
}
|
|
634
|
+
assertPolicySelectorOption(workspace, options, "skillboard guard use <skill-id>", config);
|
|
641
635
|
const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
|
|
642
636
|
if (options.get("json") === "true") {
|
|
643
637
|
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -1272,17 +1266,39 @@ function statePaths(options) {
|
|
|
1272
1266
|
});
|
|
1273
1267
|
}
|
|
1274
1268
|
|
|
1275
|
-
function
|
|
1276
|
-
if (workspace.version !== 2) return;
|
|
1269
|
+
function assertPolicySelectorOption(workspace, options, command, config) {
|
|
1277
1270
|
const agent = options.get("agent");
|
|
1271
|
+
const workflow = options.get("workflow");
|
|
1272
|
+
if (workspace.version === 1) {
|
|
1273
|
+
if (agent !== undefined) {
|
|
1274
|
+
throw new Error([
|
|
1275
|
+
"This workspace uses a version 1 policy, which selects with --workflow <name>.",
|
|
1276
|
+
`Either pass --workflow, or preview migration with: skillboard migrate v2 --config ${shellQuote(config)} --json`
|
|
1277
|
+
].join("\n"));
|
|
1278
|
+
}
|
|
1279
|
+
if (workflow === undefined) {
|
|
1280
|
+
throw new Error(selectorUsage(command));
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (workflow !== undefined) {
|
|
1285
|
+
throw new Error([
|
|
1286
|
+
"This workspace uses a version 2 policy, which selects with --agent <name>.",
|
|
1287
|
+
"Pass --agent instead of --workflow."
|
|
1288
|
+
].join("\n"));
|
|
1289
|
+
}
|
|
1278
1290
|
if (agent === undefined) {
|
|
1279
|
-
throw new Error(`Version 2 availability requires --agent.
|
|
1291
|
+
throw new Error(`Version 2 availability requires --agent. ${selectorUsage(command)}`);
|
|
1280
1292
|
}
|
|
1281
1293
|
if (!supportedAgentNames().includes(agent)) {
|
|
1282
1294
|
throw new Error(`Unsupported agent: ${agent}. Expected one of: ${supportedAgentNames().join(", ")}.`);
|
|
1283
1295
|
}
|
|
1284
1296
|
}
|
|
1285
1297
|
|
|
1298
|
+
function selectorUsage(command) {
|
|
1299
|
+
return `Usage: ${command} --agent <name> (v2 policy) | --workflow <name> (v1 policy)`;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1286
1302
|
const VALUE_OPTIONS = new Set([
|
|
1287
1303
|
"actual-harnesses", "adapted-file", "adapted-for", "agent", "cache-dir", "capability", "category",
|
|
1288
1304
|
"command", "config", "config-file", "dir", "exposure", "from", "harness", "harness-status",
|
|
@@ -1324,10 +1340,6 @@ function formatList(values) {
|
|
|
1324
1340
|
return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
|
|
1325
1341
|
}
|
|
1326
1342
|
|
|
1327
|
-
function shellQuote(value) {
|
|
1328
|
-
return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
1343
|
function readCsv(value) {
|
|
1332
1344
|
if (value === undefined || value.trim() === "") {
|
|
1333
1345
|
return [];
|
|
@@ -1617,7 +1629,15 @@ function renderRoute(result) {
|
|
|
1617
1629
|
if (result.matched_capability === null) {
|
|
1618
1630
|
lines.push("Possible skills:");
|
|
1619
1631
|
for (const skill of result.possible_skills.slice(0, 5)) {
|
|
1620
|
-
|
|
1632
|
+
if (result.selection_mode === "model") {
|
|
1633
|
+
const description = skill.description ?? "no description";
|
|
1634
|
+
const preference = skill.preference === null || skill.preference === undefined
|
|
1635
|
+
? "no saved preference"
|
|
1636
|
+
: `saved preference=${JSON.stringify(skill.preference)}`;
|
|
1637
|
+
lines.push(`- ${skill.id}: ${description} (${preference}, allowed=${skill.allowed})`);
|
|
1638
|
+
} else {
|
|
1639
|
+
lines.push(`- ${skill.id} (${skill.category ?? "uncategorized"}, allowed=${skill.allowed})`);
|
|
1640
|
+
}
|
|
1621
1641
|
}
|
|
1622
1642
|
}
|
|
1623
1643
|
lines.push("");
|
|
@@ -1689,6 +1709,8 @@ function renderDoctorSummary(result) {
|
|
|
1689
1709
|
`SkillBoard doctor: ${status}`,
|
|
1690
1710
|
renderInstallation(result.installation),
|
|
1691
1711
|
`Workspace: ${result.workspace.skills.declared} skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
|
|
1712
|
+
`Inventory integrity: ${result.inventory.ok ? "passed" : "failed"} (${result.inventory.errors.length} errors)`,
|
|
1713
|
+
`Stale removed-skill policy: ${result.inventory.stalePolicySkills.length}`,
|
|
1692
1714
|
`Source audit: ${result.sources.ok ? "passed" : "failed"} (${result.sources.errors.length} errors, ${result.sources.warnings.length} warnings, ${result.sources.blockingWarnings.length} blocking)`,
|
|
1693
1715
|
`Policy: ${result.policy.ok ? "passed" : "failed"} (${result.policy.errors.length} errors, ${result.policy.warnings.length} warnings)`
|
|
1694
1716
|
];
|
|
@@ -1726,6 +1748,8 @@ function renderDoctor(result) {
|
|
|
1726
1748
|
`Config: ${result.config.exists ? result.config.valid ? `valid v${result.config.version}` : `invalid (${result.config.error})` : "missing"}`,
|
|
1727
1749
|
`Bridge: ${bridges}`,
|
|
1728
1750
|
`Workspace: ${result.workspace.skills.declared} declared skills, ${result.workspace.skills.installed} installed skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
|
|
1751
|
+
`Inventory integrity: ${result.inventory.ok ? "passed" : "failed"} (${result.inventory.errors.length} errors)`,
|
|
1752
|
+
`Stale removed-skill policy (${result.inventory.stalePolicySkills.length}): ${formatList(result.inventory.stalePolicySkills)}`,
|
|
1729
1753
|
`Skill states: ${renderCounts(result.workspace.skills.byStatus)}`,
|
|
1730
1754
|
`Invocations: ${renderCounts(result.workspace.skills.byInvocation)}`,
|
|
1731
1755
|
`Source classes: ${renderCounts(result.workspace.installUnits.bySourceClass)}`,
|
|
@@ -1804,9 +1828,9 @@ function helpText() {
|
|
|
1804
1828
|
" check --config <path> --skills <dir>",
|
|
1805
1829
|
" list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]",
|
|
1806
1830
|
" explain <skill-id> [--config <path>] [--skills <dir>] [--json]",
|
|
1807
|
-
" route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
1808
|
-
" can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
1809
|
-
" guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
1831
|
+
" route <intent> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1832
|
+
" can-use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1833
|
+
" guard use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1810
1834
|
"",
|
|
1811
1835
|
"Advanced operator commands:",
|
|
1812
1836
|
" Import, audit, rollout, hook, lock, variant, reconcile, impact, dashboard, and v1 lifecycle commands remain available through command-specific help and docs/reference.md.",
|
|
@@ -1818,8 +1842,10 @@ function helpText() {
|
|
|
1818
1842
|
"",
|
|
1819
1843
|
"v2 AI/automation control loop:",
|
|
1820
1844
|
" Policy decides only enabled/disabled and per-skill opt-in sharing; see docs/ai-skill-routing-goal.md.",
|
|
1821
|
-
" Valid installed skills default to enabled and agent-local. Optional preference
|
|
1822
|
-
" Read brief --intent --agent <agent
|
|
1845
|
+
" Valid installed skills default to enabled and agent-local. Optional preference is raw model context and never changes availability.",
|
|
1846
|
+
" Read brief --intent --agent <agent>; the model selects from raw eligible skill descriptions and saved preferences, or uses no skill.",
|
|
1847
|
+
" SkillBoard does not tokenize, score, match, or recommend from v2 request text.",
|
|
1848
|
+
" Run guard use automatically before using the model-selected skill.",
|
|
1823
1849
|
" For an allowed skill, work without another approval; ask once only before a persistent policy change.",
|
|
1824
1850
|
" Source/provenance observations are audit metadata, never availability. Runtime/action authorization is outside SkillBoard.",
|
|
1825
1851
|
" Stale v1 policy is read-only: preview with skillboard migrate v2 --config <path> --json, apply with --yes --json, or restore with --rollback <backup> --json.",
|
|
@@ -2053,7 +2079,7 @@ function briefHelpText() {
|
|
|
2053
2079
|
"",
|
|
2054
2080
|
"Options:",
|
|
2055
2081
|
" --agent <name> Evaluate actual installation for the current agent; required for v2 availability and routing.",
|
|
2056
|
-
" --intent <request>
|
|
2082
|
+
" --intent <request> Pass the request beside raw eligible descriptions and saved preferences; v2 does not score it.",
|
|
2057
2083
|
" --config <path> Advanced policy override; defaults to ~/skillboard.config.yaml.",
|
|
2058
2084
|
" --skills <dir> Use a specific skills directory.",
|
|
2059
2085
|
" --include-actions Include current action ids in JSON output.",
|
|
@@ -2071,37 +2097,41 @@ function briefHelpText() {
|
|
|
2071
2097
|
|
|
2072
2098
|
function routeHelpText() {
|
|
2073
2099
|
return [
|
|
2074
|
-
"Usage: skillboard route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2100
|
+
"Usage: skillboard route <intent> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2075
2101
|
"",
|
|
2076
|
-
"
|
|
2077
|
-
"
|
|
2102
|
+
"In v2, returns raw eligible skill descriptions and saved preferences without interpreting the request.",
|
|
2103
|
+
"The model makes the semantic choice, including explicit requests and preferences, without changing policy.",
|
|
2104
|
+
"Version 1 compatibility routing retains its legacy deterministic tokenizer and recommendation behavior.",
|
|
2078
2105
|
"",
|
|
2079
2106
|
"Options:",
|
|
2080
2107
|
" <intent> Natural-language request, such as \"write tests first\".",
|
|
2081
|
-
" --agent <name>
|
|
2108
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2109
|
+
" Route with the selector required by the detected policy version.",
|
|
2082
2110
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2083
2111
|
" --skills <dir> Use a specific skills directory.",
|
|
2084
2112
|
" --json Print an agent-readable payload.",
|
|
2085
2113
|
"",
|
|
2086
2114
|
"AI use:",
|
|
2087
|
-
"
|
|
2115
|
+
" In v2, choose from the eligible skill descriptions and raw saved preferences, or use no skill.",
|
|
2116
|
+
" Run the guard automatically before invoking the model-selected skill.",
|
|
2088
2117
|
" If the guard allows use, disclose the skill at start and completion; do not ask for another approval.",
|
|
2089
|
-
" If
|
|
2090
|
-
"
|
|
2118
|
+
" If a saved preference would help later model selection, ask after completion whether to remember it.",
|
|
2119
|
+
" In v1 compatibility mode, ask a clarifying question when no workflow route matches.",
|
|
2091
2120
|
""
|
|
2092
2121
|
].join("\n");
|
|
2093
2122
|
}
|
|
2094
2123
|
|
|
2095
2124
|
function canUseHelpText() {
|
|
2096
2125
|
return [
|
|
2097
|
-
"Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2126
|
+
"Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2098
2127
|
"",
|
|
2099
2128
|
"Checks whether one skill is enabled and installed for the selected agent without changing policy.",
|
|
2100
2129
|
"Use it when the AI needs an availability answer for a named skill.",
|
|
2101
2130
|
"",
|
|
2102
2131
|
"Options:",
|
|
2103
2132
|
" <skill-id> Skill id to check.",
|
|
2104
|
-
" --agent <name>
|
|
2133
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2134
|
+
" Select the agent or legacy workflow that would use the skill.",
|
|
2105
2135
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2106
2136
|
" --skills <dir> Use a specific skills directory.",
|
|
2107
2137
|
" --json Print an agent-readable payload.",
|
|
@@ -2116,7 +2146,7 @@ function canUseHelpText() {
|
|
|
2116
2146
|
|
|
2117
2147
|
function guardHelpText() {
|
|
2118
2148
|
return [
|
|
2119
|
-
"Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2149
|
+
"Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2120
2150
|
"",
|
|
2121
2151
|
"Checks whether one skill may be used right now.",
|
|
2122
2152
|
"Run this immediately before the AI invokes a skill.",
|
|
@@ -2124,7 +2154,8 @@ function guardHelpText() {
|
|
|
2124
2154
|
"Options:",
|
|
2125
2155
|
" use Guard a skill invocation.",
|
|
2126
2156
|
" <skill-id> Skill id to check.",
|
|
2127
|
-
" --agent <name>
|
|
2157
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2158
|
+
" Select the agent or legacy workflow that will use the skill.",
|
|
2128
2159
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2129
2160
|
" --skills <dir> Use a specific skills directory.",
|
|
2130
2161
|
" --json Print an agent-readable payload.",
|
package/src/doctor.mjs
CHANGED
|
@@ -47,7 +47,7 @@ export async function doctorProject(options = {}) {
|
|
|
47
47
|
bridges,
|
|
48
48
|
installation,
|
|
49
49
|
workspace: emptyWorkspaceSummary(),
|
|
50
|
-
inventory: { required: false, ok: true, path: null, errors: [] },
|
|
50
|
+
inventory: { required: false, ok: true, path: null, errors: [], stalePolicySkills: [] },
|
|
51
51
|
policy: { ok: false, errors: [], warnings: [] },
|
|
52
52
|
sources: { checked: false, verified: options.verifySources === true, ok: false, errors: [], warnings: [], blockingWarnings: [], units: [] },
|
|
53
53
|
uninstall,
|
|
@@ -136,12 +136,9 @@ function doctorRecommendations(result) {
|
|
|
136
136
|
const recommendations = [];
|
|
137
137
|
if (!result.inventory.ok) {
|
|
138
138
|
recommendations.push("run skillboard inventory refresh and fix generated inventory integrity errors");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
recommendations.push(`reinstall ${match.groups.skill} or run skillboard skill forget ${match.groups.skill}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
139
|
+
}
|
|
140
|
+
for (const skill of result.inventory.stalePolicySkills) {
|
|
141
|
+
recommendations.push(`reinstall ${skill} or run skillboard skill forget ${skill}`);
|
|
145
142
|
}
|
|
146
143
|
if (!result.bridges.some((bridge) => bridge.status === "installed")) {
|
|
147
144
|
recommendations.push("legacy project bridge blocks are absent; run skillboard init only if maintaining deprecated project-local policy");
|
|
@@ -176,20 +173,22 @@ function doctorRecommendations(result) {
|
|
|
176
173
|
|
|
177
174
|
function inventoryHealth(workspace) {
|
|
178
175
|
if (workspace.version !== 2) {
|
|
179
|
-
return { required: false, ok: true, path: null, errors: [] };
|
|
176
|
+
return { required: false, ok: true, path: null, errors: [], stalePolicySkills: [] };
|
|
180
177
|
}
|
|
181
178
|
const errors = [...(workspace.inventory?.integrityErrors ?? ["generated inventory is unavailable"])];
|
|
182
179
|
const observed = new Set(workspace.inventory?.skillIds ?? []);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
const stalePolicySkills = errors.length === 0
|
|
181
|
+
? workspace.skills
|
|
182
|
+
.map((skill) => skill.id)
|
|
183
|
+
.filter((skillId) => !observed.has(skillId))
|
|
184
|
+
.sort((left, right) => left.localeCompare(right))
|
|
185
|
+
: [];
|
|
188
186
|
return {
|
|
189
187
|
required: true,
|
|
190
188
|
ok: errors.length === 0,
|
|
191
189
|
path: workspace.inventory?.path ?? null,
|
|
192
|
-
errors
|
|
190
|
+
errors,
|
|
191
|
+
stalePolicySkills
|
|
193
192
|
};
|
|
194
193
|
}
|
|
195
194
|
|
package/src/lifecycle-cli.mjs
CHANGED
|
@@ -140,7 +140,7 @@ function writeSafetyDefault(stdout, safety) {
|
|
|
140
140
|
stdout.write("Skill selection default:\n");
|
|
141
141
|
stdout.write("- Valid installed skills default to enabled and agent-local.\n");
|
|
142
142
|
stdout.write("- Users may disable a skill or explicitly share that skill across agents.\n");
|
|
143
|
-
stdout.write("- Optional preference
|
|
143
|
+
stdout.write("- Optional preference is raw model context and never changes availability.\n");
|
|
144
144
|
stdout.write("- Source and provenance are audit metadata, never availability.\n");
|
|
145
145
|
stdout.write(`- ${safety.enabled} enabled skills\n`);
|
|
146
146
|
stdout.write(`- ${safety.disabled} disabled skills\n`);
|
|
@@ -13,27 +13,29 @@ export function bridgeBlock() {
|
|
|
13
13
|
|
|
14
14
|
SkillBoard keeps one user-level policy. A valid installed skill defaults to
|
|
15
15
|
enabled and agent-local. Users may opt individual skills into cross-agent
|
|
16
|
-
sharing. Optional preference
|
|
17
|
-
|
|
16
|
+
sharing. Optional preference is raw context for the model and never changes
|
|
17
|
+
availability or copies files.
|
|
18
18
|
|
|
19
19
|
## Route and use
|
|
20
20
|
|
|
21
21
|
- Work normally unless skills overlap or the user asks
|
|
22
22
|
for a skill decision.
|
|
23
23
|
- Read \`skillboard brief --intent <request> --agent <agent> --json\`.
|
|
24
|
-
-
|
|
25
|
-
|
|
24
|
+
- SkillBoard returns eligible skill descriptions and raw saved preferences. It
|
|
25
|
+
does not tokenize, score, match, or recommend from v2 request text.
|
|
26
|
+
- The model makes the semantic choice, including explicit user requests and
|
|
27
|
+
saved preferences, or continues without a skill when none fits.
|
|
26
28
|
- Run \`skillboard guard use <skill-id> --agent <agent> --json\` immediately
|
|
27
29
|
before use.
|
|
28
30
|
- Do not ask for another approval when guard allows use. Briefly disclose the
|
|
29
31
|
selected skill as an audit trace.
|
|
30
|
-
- If the
|
|
32
|
+
- If remembering the choice would help later model selection, finish the work first, then ask
|
|
31
33
|
once whether to remember that intent preference. Do not prompt before use.
|
|
32
34
|
|
|
33
35
|
## Change policy
|
|
34
36
|
|
|
35
37
|
- Policy changes only enable/disable a skill, share/unshare one skill, or
|
|
36
|
-
remember optional
|
|
38
|
+
remember optional model preference.
|
|
37
39
|
- Read a fresh brief with \`--include-actions\`, choose one current action id, ask
|
|
38
40
|
for one confirmation, then run \`skillboard apply-action <action-id> --yes
|
|
39
41
|
--json\`.
|
package/src/route-advisory.mjs
CHANGED
|
@@ -119,50 +119,6 @@ export function postUsePolicySuggestionForCapabilityRoute({ matchedCapability, r
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
export function policyMemoryForV2Route({ intent, selected, candidates, workflowName }) {
|
|
123
|
-
if (selected.explicit || selected.matchedIntents.length === 0 || candidates.length < 2) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
const alternatives = candidates.slice(1).map((candidate) => candidate.skill.id);
|
|
127
|
-
const alternativeText = alternatives.join(", ");
|
|
128
|
-
const context = workflowName === undefined ? "for the current agent" : `in legacy workflow ${workflowName}`;
|
|
129
|
-
return {
|
|
130
|
-
status: "applied",
|
|
131
|
-
mode: "remembered-or-configured-preference",
|
|
132
|
-
selected_skill: selected.skill.id,
|
|
133
|
-
available_alternatives: alternatives,
|
|
134
|
-
summary: `Remembered or configured policy selected ${selected.skill.id} for ${intent} ${context}; other allowed skills were also available: ${alternativeText}.`,
|
|
135
|
-
finish_disclosure: `I used ${selected.skill.id} for this request because SkillBoard has a remembered or configured preference for it; other allowed skills were also available: ${alternativeText}.`
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function postUsePolicySuggestionForV2Route({ intent, selected, candidates, workflowName, options, policyMemory }) {
|
|
140
|
-
if (selected.explicit || policyMemory !== null || candidates.length < 2) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
const context = workflowName === undefined ? "" : ` in ${workflowName}`;
|
|
144
|
-
return {
|
|
145
|
-
timing: "after_use",
|
|
146
|
-
mode: "ask_after_use",
|
|
147
|
-
reason: `SkillBoard found multiple allowed skills for ${intent} and selected ${selected.skill.id}. After completing the task, ask whether to remember ${selected.skill.id} for similar requests${context}.`,
|
|
148
|
-
question: `Should I remember ${selected.skill.id} as the preferred skill for similar ${intent} requests${context}?`,
|
|
149
|
-
requires_confirmation: true,
|
|
150
|
-
suggested_policy: {
|
|
151
|
-
kind: "prefer-skill",
|
|
152
|
-
skill: selected.skill.id,
|
|
153
|
-
workflow: workflowName ?? null,
|
|
154
|
-
intent,
|
|
155
|
-
command_hint: command([
|
|
156
|
-
"skillboard", "skill", "preference", selected.skill.id,
|
|
157
|
-
"--intent", intent,
|
|
158
|
-
"--priority", "100",
|
|
159
|
-
"--config", routeConfigPath(options),
|
|
160
|
-
...(options.skillsRoot === undefined ? [] : ["--skills", routeSkillsRoot(options)])
|
|
161
|
-
]).display
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
122
|
function postUsePolicySuggestionForDeniedPreferredFallback({ matchedCapability, recommended, routedSkills, workflowName, options }) {
|
|
167
123
|
if (recommended === null || recommended.role !== "fallback" || recommended.guard.allowed !== true) {
|
|
168
124
|
return null;
|
package/src/route-renderer.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function renderRouteSectionLines(route, options = {}) {
|
|
|
30
30
|
const lines = [];
|
|
31
31
|
pushRouteMatchLines(lines, route, formatter, options);
|
|
32
32
|
if (route.recommended_skill === null) {
|
|
33
|
-
pushNoRecommendationLines(lines, route, formatter);
|
|
33
|
+
pushNoRecommendationLines(lines, route, formatter, options);
|
|
34
34
|
return lines;
|
|
35
35
|
}
|
|
36
36
|
pushRecommendationLines(lines, route, formatter, options);
|
|
@@ -50,11 +50,18 @@ function pushRouteMatchLines(lines, route, formatter, options) {
|
|
|
50
50
|
lines.push(`${formatter.prefix}Matched terms: ${formatter.list(route.matched_terms)}`);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function pushNoRecommendationLines(lines, route, formatter) {
|
|
53
|
+
function pushNoRecommendationLines(lines, route, formatter, options) {
|
|
54
54
|
lines.push(`${formatter.prefix}Recommended skill: none`);
|
|
55
55
|
if (formatter.includeFallbackWhenNoRecommendation) {
|
|
56
56
|
lines.push(`${formatter.prefix}Fallback skills: ${formatter.list(route.fallback_skills)}`);
|
|
57
57
|
}
|
|
58
|
+
if (route.model_selection_required === true) {
|
|
59
|
+
lines.push(`${formatter.prefix}Model selection: required - choose from eligible skill descriptions and raw saved preferences, or use no skill.`);
|
|
60
|
+
if (typeof options.nextStep === "string" && options.nextStep.length > 0) {
|
|
61
|
+
lines.push(`${formatter.prefix}Next step: ${safeText(options.nextStep, 360)}`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
58
65
|
if (formatter.noMatchNextStep !== null) {
|
|
59
66
|
lines.push(`${formatter.prefix}Next step: ${formatter.noMatchNextStep}`);
|
|
60
67
|
}
|
package/src/route-selection.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { canonicalRouteToken, phraseKey, tokensFor } from "./route-tokens.mjs";
|
|
|
3
3
|
|
|
4
4
|
const HIGH_CONFIDENCE = 4;
|
|
5
5
|
const MEDIUM_CONFIDENCE = 2;
|
|
6
|
+
const CJK_SUFFIX = /^[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]+$/u;
|
|
6
7
|
|
|
7
8
|
export function selectRoute(workspace, workflow, intent) {
|
|
8
9
|
const intentTokens = tokensFor(intent);
|
|
@@ -24,38 +25,6 @@ export function selectRoute(workspace, workflow, intent) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function selectV2Route(workspace, intent, agentName) {
|
|
28
|
-
const intentKey = phraseKey(intent);
|
|
29
|
-
const intentTokens = tokensFor(intent);
|
|
30
|
-
const candidates = workspace.skills
|
|
31
|
-
.map((skill) => {
|
|
32
|
-
const guard = canUseSkill(workspace, skill.id, undefined, agentName);
|
|
33
|
-
const explicit = exactSkillIdMention(intentKey, skill.id);
|
|
34
|
-
const preference = skill.preference;
|
|
35
|
-
const matchedIntents = preference?.intents.filter((term) => matchesIntentTerm(intentTokens, term)) ?? [];
|
|
36
|
-
const installed = installedSkillFor(workspace, skill.id);
|
|
37
|
-
const metadataTokens = tokensFor(`${installed?.name ?? ""} ${installed?.description ?? ""}`);
|
|
38
|
-
const metadataMatches = [...metadataTokens].filter((token) => intentTokens.has(token));
|
|
39
|
-
const hasMatch = explicit || matchedIntents.length > 0 || metadataMatches.length > 0;
|
|
40
|
-
const preferenceScore = matchedIntents.length > 0 ? (preference?.priority ?? 0) : 0;
|
|
41
|
-
const rawScore = (explicit ? 1_000_000 : 0)
|
|
42
|
-
+ matchedIntents.length * 100
|
|
43
|
-
+ preferenceScore
|
|
44
|
-
+ metadataMatches.length;
|
|
45
|
-
const score = hasMatch ? Math.max(1, rawScore) : 0;
|
|
46
|
-
return { skill, guard, explicit, score, matchedIntents, metadataMatches };
|
|
47
|
-
})
|
|
48
|
-
.filter((candidate) => candidate.guard.allowed)
|
|
49
|
-
.filter((candidate) => candidate.score > 0)
|
|
50
|
-
.sort((left, right) => right.score - left.score || left.skill.id.localeCompare(right.skill.id));
|
|
51
|
-
return { candidates, selected: candidates[0] };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function matchesIntentTerm(intentTokens, term) {
|
|
55
|
-
const termTokens = tokensFor(term);
|
|
56
|
-
return termTokens.size > 0 && [...termTokens].every((token) => intentTokens.has(token));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
28
|
function explicitAllowedSkillRouteCandidate(intent, skillCandidates) {
|
|
60
29
|
const intentKey = phraseKey(intent);
|
|
61
30
|
return skillCandidates.find((candidate) => candidate.allowed && exactSkillIdMention(intentKey, candidate.skill_id));
|
|
@@ -66,7 +35,16 @@ function exactSkillIdMention(intentKey, skillId) {
|
|
|
66
35
|
if (skillKey.length === 0) return false;
|
|
67
36
|
const intentTokens = intentKey.split(" ");
|
|
68
37
|
const skillTokens = skillKey.split(" ");
|
|
69
|
-
|
|
38
|
+
const lastSkillToken = skillTokens.length - 1;
|
|
39
|
+
return intentTokens.some((_, index) => skillTokens.every((token, offset) => exactSkillTokenMatch(
|
|
40
|
+
intentTokens[index + offset], token, offset === lastSkillToken
|
|
41
|
+
)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function exactSkillTokenMatch(intentToken, skillToken, allowCjkSuffix) {
|
|
45
|
+
if (intentToken === skillToken) return true;
|
|
46
|
+
if (!allowCjkSuffix || !intentToken?.startsWith(skillToken)) return false;
|
|
47
|
+
return CJK_SUFFIX.test(intentToken.slice(skillToken.length));
|
|
70
48
|
}
|
|
71
49
|
|
|
72
50
|
function capabilityRouteCandidates(workspace, workflow) {
|
package/src/route-tokens.mjs
CHANGED
|
@@ -32,24 +32,42 @@ const ROUTE_STOP_WORDS = new Set([
|
|
|
32
32
|
"your"
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
const ASCII_ROUTE_TOKEN = /^[a-z0-9]+$/u;
|
|
36
|
+
const CJK_ROUTE_TOKEN = /[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
37
|
+
|
|
35
38
|
export function tokensFor(value) {
|
|
36
39
|
return new Set(String(value)
|
|
37
40
|
.toLowerCase()
|
|
38
|
-
.split(/[
|
|
41
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
39
42
|
.flatMap(tokenForms)
|
|
40
43
|
.filter(isRouteToken));
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
function tokenForms(token) {
|
|
47
|
+
if (CJK_ROUTE_TOKEN.test(token)) {
|
|
48
|
+
return cjkTokenForms(token);
|
|
49
|
+
}
|
|
44
50
|
const singular = singularRouteToken(token);
|
|
45
51
|
return singular === token ? [token] : [token, singular];
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function cjkTokenForms(token) {
|
|
55
|
+
const characters = [...token];
|
|
56
|
+
const forms = [token];
|
|
57
|
+
for (let index = 0; index < characters.length - 1; index += 1) {
|
|
58
|
+
forms.push(characters.slice(index, index + 2).join(""));
|
|
59
|
+
}
|
|
60
|
+
return forms;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function canonicalRouteToken(token) {
|
|
49
64
|
return singularRouteToken(token);
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
function singularRouteToken(token) {
|
|
68
|
+
if (!ASCII_ROUTE_TOKEN.test(token)) {
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
53
71
|
if (token.length > 4 && token.endsWith("ies")) {
|
|
54
72
|
return `${token.slice(0, -3)}y`;
|
|
55
73
|
}
|
|
@@ -60,7 +78,7 @@ function singularRouteToken(token) {
|
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
function isRouteToken(token) {
|
|
63
|
-
return token.length > 1 && !ROUTE_STOP_WORDS.has(token);
|
|
81
|
+
return (token.length > 1 || CJK_ROUTE_TOKEN.test(token)) && !ROUTE_STOP_WORDS.has(token);
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
export function phraseKey(value) {
|
package/src/route.mjs
CHANGED
|
@@ -4,15 +4,13 @@ import {
|
|
|
4
4
|
guardCommand,
|
|
5
5
|
overlapResolutionForRoute,
|
|
6
6
|
policyMemoryForRoute,
|
|
7
|
-
policyMemoryForV2Route,
|
|
8
7
|
postUsePolicySuggestionForCapabilityRoute,
|
|
9
|
-
postUsePolicySuggestionForV2Route,
|
|
10
8
|
recommendationReason,
|
|
11
9
|
routeCandidate,
|
|
12
10
|
selectedRouteSkill,
|
|
13
11
|
usageDisclosure
|
|
14
12
|
} from "./route-advisory.mjs";
|
|
15
|
-
import { confidenceFor, possibleWorkflowSkills, selectRoute
|
|
13
|
+
import { confidenceFor, possibleWorkflowSkills, selectRoute } from "./route-selection.mjs";
|
|
16
14
|
|
|
17
15
|
export function routeSkill(workspace, options) {
|
|
18
16
|
const intent = options.intent.trim();
|
|
@@ -103,57 +101,42 @@ export function routeSkill(workspace, options) {
|
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
function routeV2Skill(workspace, options, intent) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
matched_terms: [], recommendation_reason: "No enabled skill installed for this agent matched this request.",
|
|
112
|
-
recommended_skill: null, fallback_skills: [], route_candidates: [], overlap_resolution: null,
|
|
113
|
-
policy_memory: null, guard_command: null, guard: null, usage_disclosure: null,
|
|
114
|
-
post_use_policy_suggestion: null, possible_skills: [], candidates: []
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const routed = candidates.map((candidate) => ({
|
|
118
|
-
skill: candidate.skill.id,
|
|
119
|
-
role: candidate.explicit ? "matched" : "alternative",
|
|
120
|
-
guard: candidate.guard
|
|
121
|
-
}));
|
|
122
|
-
const matchedTerms = [...new Set([...selected.matchedIntents, ...selected.metadataMatches])];
|
|
123
|
-
const contextName = options.agent === undefined ? "the current agent" : `agent ${options.agent}`;
|
|
124
|
-
const recommended = routed[0];
|
|
125
|
-
const overlapResolution = overlapResolutionForRoute({
|
|
126
|
-
matchedCapability: intent,
|
|
127
|
-
recommended,
|
|
128
|
-
routedSkills: routed,
|
|
129
|
-
workflowName: contextName
|
|
130
|
-
});
|
|
131
|
-
const policyMemory = policyMemoryForV2Route({ intent, selected, candidates, workflowName: undefined });
|
|
132
|
-
const postUsePolicySuggestion = postUsePolicySuggestionForV2Route({
|
|
133
|
-
intent, selected, candidates, workflowName: undefined, options, policyMemory
|
|
134
|
-
});
|
|
104
|
+
const possibleSkills = workspace.skills
|
|
105
|
+
.filter((skill) => canUseSkill(workspace, skill.id, undefined, options.agent).allowed)
|
|
106
|
+
.map((skill) => v2ModelSkill(workspace, skill))
|
|
107
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
108
|
+
const modelSelectionRequired = possibleSkills.length > 0;
|
|
135
109
|
return {
|
|
136
110
|
ok: true, intent, workflow: null, agent: options.agent ?? null,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
match_source:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
111
|
+
selection_mode: "model",
|
|
112
|
+
model_selection_required: modelSelectionRequired,
|
|
113
|
+
matched_capability: null, matched_skill: null, match_source: "none",
|
|
114
|
+
confidence: "none", matched_terms: [],
|
|
115
|
+
recommendation_reason: modelSelectionRequired
|
|
116
|
+
? "SkillBoard does not interpret v2 request text. The active model should choose from the eligible skill descriptions and saved preferences, or use no skill when none fits."
|
|
117
|
+
: "No enabled skill is installed for the selected agent.",
|
|
118
|
+
recommended_skill: null, fallback_skills: [], route_candidates: [],
|
|
119
|
+
overlap_resolution: null, policy_memory: null,
|
|
120
|
+
guard_command: null, guard: null, usage_disclosure: null,
|
|
121
|
+
post_use_policy_suggestion: null,
|
|
122
|
+
possible_skills: possibleSkills,
|
|
123
|
+
candidates: []
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function v2ModelSkill(workspace, skill) {
|
|
128
|
+
const inventory = workspace.inventory?.skills?.find((candidate) => candidate.id === skill.id);
|
|
129
|
+
const installed = workspace.installedSkills.find((candidate) => (
|
|
130
|
+
candidate.id === skill.id
|
|
131
|
+
|| (typeof skill.path === "string" && candidate.path === skill.path)
|
|
132
|
+
));
|
|
133
|
+
return {
|
|
134
|
+
id: skill.id,
|
|
135
|
+
name: installed?.name ?? inventory?.name ?? skill.id,
|
|
136
|
+
description: installed?.description ?? inventory?.description ?? null,
|
|
137
|
+
path: installed?.path ?? inventory?.path ?? null,
|
|
138
|
+
preference: skill.preference ?? null,
|
|
139
|
+
allowed: true
|
|
157
140
|
};
|
|
158
141
|
}
|
|
159
142
|
|