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 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. Optional preference ranks enabled skills installed for the current agent
10
- and never changes availability or copies files.
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.2 writes policy schema v2.
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 an enabled skill installed for that agent, using preference only to
132
- rank matches.
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 routing preference
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 ranks
11
- enabled candidates that are actually installed for the current agent;
12
- preference never changes availability or copies files.
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, run the guard before use, and ask
19
- once only before a persistent policy change.
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, or legacy state fields as hidden
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.
@@ -24,7 +24,7 @@ compatible copies for other supported agents.
24
24
 
25
25
  ## Optional routing preference
26
26
 
27
- Preference ranks enabled skills installed for the current agent. It never
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
- An explicit user selection wins when the guard allows it. A high priority cannot
42
- enable a disabled skill or make a skill appear on an agent where it is absent.
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`.
@@ -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
- Optional preference ranks enabled skills installed for the current agent and
12
- never changes availability.
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 codex|claude|opencode|hermes [--json]
20
- skillboard can-use <skill-id> --agent codex|claude|opencode|hermes [--json]
21
- skillboard guard use <skill-id> --agent codex|claude|opencode|hermes [--json]
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`. Optional preference ranks candidates but never changes
54
- availability.
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 and ranks enabled skills installed for the current
83
- agent only.
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
- ## Resolution order
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. Honor an explicit user-selected skill if it remains eligible.
13
- 5. Rank intent matches by optional preference and priority.
14
- 6. Return one recommendation and ordered fallbacks.
15
-
16
- Preference ranks only and never changes availability.
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. Ask after completion
30
- only when remembering a preference would reduce future ambiguity.
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. Preference ranks matching candidates and never changes
45
- availability. Allowed use does not require another confirmation.
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 ask whether to
69
- remember an intent preference. Preference affects ordering only; it does not
70
- share, enable, disable, install, or remove a skill.
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
 
@@ -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
- - Optional preference changes ranking without changing availability.
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 ranks it and never changes availability.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Keep agent skills broadly available while routing overlaps consistently.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -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 use optional preference only to rank available overlaps.",
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
- ? "Ask a clarifying question; no workflow capability matched this request."
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
  }))
@@ -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 SkillBoard says remembered or configured policy selected this skill while other allowed skills were available, mention that at completion.
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. Prefer the skill whose explicit request, description, and local instructions most directly match the user's task.
45
- 4. Use \`skillboard brief --agent ${agent} --intent <request> --json\` or \`skillboard route <intent> --agent ${agent} --json\` to break ties.
46
- 5. Run \`skillboard guard use <skill-id> --agent ${agent} --json\` immediately before invoking the selected skill.
47
- 6. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
48
- 7. Continue with the selected skill; do not stop only because other candidate skills exist.
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
 
@@ -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("## Suggested skill for this request", "");
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 codex|claude|opencode|hermes [--json]"]],
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("Usage: skillboard route <intent> [--agent <name>]");
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
- assertV2AgentOption(workspace, options, "skillboard route <intent> --agent <name>");
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("Usage: skillboard can-use <skill-id> [--agent <name>]");
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("Usage: skillboard guard use <skill-id> [--agent <name>]");
622
+ throw new Error(selectorUsage("skillboard guard use <skill-id>"));
627
623
  }
628
- const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
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
- assertV2AgentOption(workspace, options, "skillboard guard use <skill-id> --agent <name>");
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 assertV2AgentOption(workspace, options, usage) {
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. Usage: ${usage}`);
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
- lines.push(`- ${skill.id} (${skill.category ?? "uncategorized"}, allowed=${skill.allowed})`);
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 ranks only and never changes availability.",
1822
- " Read brief --intent --agent <agent>, select an enabled installed skill, and run guard use automatically before use.",
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> Add a natural-language request so SkillBoard can suggest a skill.",
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
- "Suggests the routed skill for a user request when several allowed skills may overlap.",
2077
- "Use it when the AI needs a skill recommendation without changing policy.",
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> Route among skills installed for this agent.",
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
- " If a skill is recommended, run the guard automatically before invoking it.",
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 policy memory would reduce ambiguity, ask after completion whether to remember the routed skill.",
2090
- " If no skill matches, ask a clarifying question instead of guessing.",
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> Agent that would use the skill.",
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> Agent that will use the skill.",
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
- for (const error of result.inventory.errors) {
140
- const match = /^skill (?<skill>[^ ]+) is missing from generated inventory$/u.exec(error);
141
- if (match?.groups?.skill !== undefined) {
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
- for (const skill of workspace.skills) {
184
- if (!observed.has(skill.id)) {
185
- errors.push(`skill ${skill.id} is missing from generated inventory`);
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
 
@@ -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 ranks candidates and never changes availability.\n");
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 ranks enabled skills installed for the current
17
- agent and never changes availability or copies files.
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
- - Use the recommended enabled, installed skill and ordered fallbacks. Honor an
25
- explicit user choice when the guard allows it.
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 routed brief suggests policy memory, finish the work first, then ask
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 ranking preference.
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\`.
@@ -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;
@@ -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
  }
@@ -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
- return intentTokens.some((_, index) => skillTokens.every((token, offset) => intentTokens[index + offset] === token));
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) {
@@ -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(/[^a-z0-9]+/u)
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, selectV2Route } from "./route-selection.mjs";
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 { candidates, selected } = selectV2Route(workspace, intent, options.agent);
107
- if (selected === undefined) {
108
- return {
109
- ok: true, intent, workflow: null, agent: options.agent ?? null,
110
- matched_capability: null, matched_skill: null, match_source: "none", confidence: "none",
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
- matched_capability: null,
138
- matched_skill: selected.skill.id,
139
- match_source: selected.explicit
140
- ? "explicit-skill"
141
- : selected.matchedIntents.length > 0
142
- ? "skill-preference"
143
- : "skill-metadata",
144
- confidence: confidenceFor(selected.score),
145
- matched_terms: matchedTerms,
146
- recommendation_reason: `Recommended ${selected.skill.id} because it is enabled, installed for this agent, and best matches the request.`,
147
- recommended_skill: selected.skill.id,
148
- fallback_skills: candidates.slice(1).map((candidate) => candidate.skill.id),
149
- route_candidates: routed.map((entry, index) => routeCandidate(entry, index === 0)),
150
- overlap_resolution: overlapResolution, policy_memory: policyMemory,
151
- guard_command: guardCommand(selected.skill.id, undefined, options),
152
- guard: selected.guard,
153
- usage_disclosure: usageDisclosure(selected.skill.id, policyMemory),
154
- post_use_policy_suggestion: postUsePolicySuggestion,
155
- possible_skills: routed.map((entry) => ({ id: entry.skill, allowed: true })),
156
- candidates: candidates.map((candidate) => ({ skill_id: candidate.skill.id, score: candidate.score }))
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