agent-skillboard 0.3.1 → 0.3.3
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 +41 -0
- package/README.md +27 -10
- package/docs/ai-skill-routing-goal.md +19 -7
- package/docs/install.md +15 -4
- package/docs/policy-model.md +12 -3
- package/docs/positioning.md +4 -2
- package/docs/reference.md +20 -9
- package/docs/routing.md +19 -8
- package/docs/user-flow.md +14 -6
- package/docs/value-proof.md +7 -2
- package/docs/variant-lifecycle.md +2 -1
- package/docs/versioning.md +9 -4
- package/package.json +1 -1
- package/src/advisor/guidance.mjs +20 -2
- package/src/agent-integration-cli.mjs +31 -48
- package/src/agent-integration-command.mjs +38 -0
- package/src/agent-integration-content.mjs +6 -6
- package/src/brief-renderer.mjs +1 -1
- package/src/cli.mjs +70 -43
- package/src/inventory-refresh.mjs +1 -0
- package/src/lifecycle-cli.mjs +1 -1
- package/src/lifecycle-content.mjs +8 -6
- package/src/migration/automatic-v2.mjs +14 -0
- package/src/migration/v2-projection.mjs +2 -1
- package/src/migration/v2-transaction.mjs +4 -0
- 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/src/setup-policy-migration.mjs +43 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function commandPrefix(runtime) {
|
|
4
|
+
const entrypoint = runtime.entrypointPath ?? "";
|
|
5
|
+
const normalized = entrypoint.replace(/\\/g, "/");
|
|
6
|
+
if (normalized.includes("/_npx/")) {
|
|
7
|
+
const packageSpec = runtime.packageSpec ?? "agent-skillboard";
|
|
8
|
+
return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
|
|
9
|
+
}
|
|
10
|
+
if (isSourceTreeEntrypoint(entrypoint)) {
|
|
11
|
+
return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
|
|
12
|
+
}
|
|
13
|
+
return "skillboard";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isSourceTreeEntrypoint(entrypoint) {
|
|
17
|
+
if (entrypoint === "") return false;
|
|
18
|
+
const normalized = entrypoint.replace(/\\/g, "/");
|
|
19
|
+
return (normalized === "bin/skillboard.mjs" || normalized.endsWith("/bin/skillboard.mjs"))
|
|
20
|
+
&& !normalized.includes("/node_modules/")
|
|
21
|
+
&& !normalized.includes("/_npx/")
|
|
22
|
+
&& !normalized.includes("/.npm/");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sourceTreeEntrypoint(entrypoint, cwd, platform) {
|
|
26
|
+
const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
|
|
27
|
+
const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
|
|
28
|
+
if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
|
|
29
|
+
return shellQuote(relativeEntrypoint, platform);
|
|
30
|
+
}
|
|
31
|
+
return shellQuote(absoluteEntrypoint, platform);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function shellQuote(value, platform = process.platform) {
|
|
35
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
36
|
+
if (platform === "win32") return `"${value.replace(/"/g, '""')}"`;
|
|
37
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
38
|
+
}
|
|
@@ -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
|
@@ -84,7 +84,7 @@ function emitIntentRoute(lines, brief) {
|
|
|
84
84
|
if (route === undefined) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
lines.push("##
|
|
87
|
+
lines.push("## Skill selection for this request", "");
|
|
88
88
|
lines.push(...renderRouteSectionLines(route, {
|
|
89
89
|
format: "brief",
|
|
90
90
|
nextStep: brief.assistant_guidance?.recommended_next_step
|
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("");
|
|
@@ -1804,9 +1824,9 @@ function helpText() {
|
|
|
1804
1824
|
" check --config <path> --skills <dir>",
|
|
1805
1825
|
" list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]",
|
|
1806
1826
|
" 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]",
|
|
1827
|
+
" route <intent> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1828
|
+
" can-use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1829
|
+
" guard use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
1810
1830
|
"",
|
|
1811
1831
|
"Advanced operator commands:",
|
|
1812
1832
|
" 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 +1838,10 @@ function helpText() {
|
|
|
1818
1838
|
"",
|
|
1819
1839
|
"v2 AI/automation control loop:",
|
|
1820
1840
|
" 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
|
|
1841
|
+
" Valid installed skills default to enabled and agent-local. Optional preference is raw model context and never changes availability.",
|
|
1842
|
+
" Read brief --intent --agent <agent>; the model selects from raw eligible skill descriptions and saved preferences, or uses no skill.",
|
|
1843
|
+
" SkillBoard does not tokenize, score, match, or recommend from v2 request text.",
|
|
1844
|
+
" Run guard use automatically before using the model-selected skill.",
|
|
1823
1845
|
" For an allowed skill, work without another approval; ask once only before a persistent policy change.",
|
|
1824
1846
|
" Source/provenance observations are audit metadata, never availability. Runtime/action authorization is outside SkillBoard.",
|
|
1825
1847
|
" 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 +2075,7 @@ function briefHelpText() {
|
|
|
2053
2075
|
"",
|
|
2054
2076
|
"Options:",
|
|
2055
2077
|
" --agent <name> Evaluate actual installation for the current agent; required for v2 availability and routing.",
|
|
2056
|
-
" --intent <request>
|
|
2078
|
+
" --intent <request> Pass the request beside raw eligible descriptions and saved preferences; v2 does not score it.",
|
|
2057
2079
|
" --config <path> Advanced policy override; defaults to ~/skillboard.config.yaml.",
|
|
2058
2080
|
" --skills <dir> Use a specific skills directory.",
|
|
2059
2081
|
" --include-actions Include current action ids in JSON output.",
|
|
@@ -2071,37 +2093,41 @@ function briefHelpText() {
|
|
|
2071
2093
|
|
|
2072
2094
|
function routeHelpText() {
|
|
2073
2095
|
return [
|
|
2074
|
-
"Usage: skillboard route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2096
|
+
"Usage: skillboard route <intent> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2075
2097
|
"",
|
|
2076
|
-
"
|
|
2077
|
-
"
|
|
2098
|
+
"In v2, returns raw eligible skill descriptions and saved preferences without interpreting the request.",
|
|
2099
|
+
"The model makes the semantic choice, including explicit requests and preferences, without changing policy.",
|
|
2100
|
+
"Version 1 compatibility routing retains its legacy deterministic tokenizer and recommendation behavior.",
|
|
2078
2101
|
"",
|
|
2079
2102
|
"Options:",
|
|
2080
2103
|
" <intent> Natural-language request, such as \"write tests first\".",
|
|
2081
|
-
" --agent <name>
|
|
2104
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2105
|
+
" Route with the selector required by the detected policy version.",
|
|
2082
2106
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2083
2107
|
" --skills <dir> Use a specific skills directory.",
|
|
2084
2108
|
" --json Print an agent-readable payload.",
|
|
2085
2109
|
"",
|
|
2086
2110
|
"AI use:",
|
|
2087
|
-
"
|
|
2111
|
+
" In v2, choose from the eligible skill descriptions and raw saved preferences, or use no skill.",
|
|
2112
|
+
" Run the guard automatically before invoking the model-selected skill.",
|
|
2088
2113
|
" If the guard allows use, disclose the skill at start and completion; do not ask for another approval.",
|
|
2089
|
-
" If
|
|
2090
|
-
"
|
|
2114
|
+
" If a saved preference would help later model selection, ask after completion whether to remember it.",
|
|
2115
|
+
" In v1 compatibility mode, ask a clarifying question when no workflow route matches.",
|
|
2091
2116
|
""
|
|
2092
2117
|
].join("\n");
|
|
2093
2118
|
}
|
|
2094
2119
|
|
|
2095
2120
|
function canUseHelpText() {
|
|
2096
2121
|
return [
|
|
2097
|
-
"Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2122
|
+
"Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2098
2123
|
"",
|
|
2099
2124
|
"Checks whether one skill is enabled and installed for the selected agent without changing policy.",
|
|
2100
2125
|
"Use it when the AI needs an availability answer for a named skill.",
|
|
2101
2126
|
"",
|
|
2102
2127
|
"Options:",
|
|
2103
2128
|
" <skill-id> Skill id to check.",
|
|
2104
|
-
" --agent <name>
|
|
2129
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2130
|
+
" Select the agent or legacy workflow that would use the skill.",
|
|
2105
2131
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2106
2132
|
" --skills <dir> Use a specific skills directory.",
|
|
2107
2133
|
" --json Print an agent-readable payload.",
|
|
@@ -2116,7 +2142,7 @@ function canUseHelpText() {
|
|
|
2116
2142
|
|
|
2117
2143
|
function guardHelpText() {
|
|
2118
2144
|
return [
|
|
2119
|
-
"Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
|
|
2145
|
+
"Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes (v2 policy) | --workflow <name> (v1 policy) [--config <path>] [--skills <dir>] [--json]",
|
|
2120
2146
|
"",
|
|
2121
2147
|
"Checks whether one skill may be used right now.",
|
|
2122
2148
|
"Run this immediately before the AI invokes a skill.",
|
|
@@ -2124,7 +2150,8 @@ function guardHelpText() {
|
|
|
2124
2150
|
"Options:",
|
|
2125
2151
|
" use Guard a skill invocation.",
|
|
2126
2152
|
" <skill-id> Skill id to check.",
|
|
2127
|
-
" --agent <name>
|
|
2153
|
+
" --agent <name> (v2 policy) | --workflow <name> (v1 policy)",
|
|
2154
|
+
" Select the agent or legacy workflow that will use the skill.",
|
|
2128
2155
|
" --config <path> Use a specific skillboard.config.yaml.",
|
|
2129
2156
|
" --skills <dir> Use a specific skills directory.",
|
|
2130
2157
|
" --json Print an agent-readable payload.",
|
|
@@ -85,6 +85,7 @@ async function refreshLocked(options) {
|
|
|
85
85
|
bootstrappedV2,
|
|
86
86
|
configPath: relativeArtifactPath(root, configPath),
|
|
87
87
|
inventoryPath: generatedInventory === null ? null : relativeArtifactPath(root, inventoryPath),
|
|
88
|
+
observedSkillIds: inventory.skills.map(({ id }) => id).sort((left, right) => left.localeCompare(right)),
|
|
88
89
|
inventoryChanged,
|
|
89
90
|
changed: plan.changed,
|
|
90
91
|
plan,
|
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\`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SAFE_AMBIGUITY_KIND = "review_only_quarantine";
|
|
2
|
+
|
|
3
|
+
export function canAutomaticallyMigrateV2(report) {
|
|
4
|
+
if (report?.mode !== "preview" || report.changed !== true || report.target_version !== 2) return false;
|
|
5
|
+
if (!Array.isArray(report.ambiguities)) return false;
|
|
6
|
+
return report.ambiguities.every((ambiguity) => (
|
|
7
|
+
ambiguity?.kind === SAFE_AMBIGUITY_KIND
|
|
8
|
+
&& ambiguity.mapped_enabled === true
|
|
9
|
+
&& ambiguity.requires_grouped_confirmation === true
|
|
10
|
+
&& Array.isArray(ambiguity.skill_ids)
|
|
11
|
+
&& ambiguity.skill_ids.length > 0
|
|
12
|
+
&& ambiguity.skill_ids.every((skillId) => typeof skillId === "string" && skillId.length > 0)
|
|
13
|
+
));
|
|
14
|
+
}
|
|
@@ -46,6 +46,9 @@ export async function migrateV2(options) {
|
|
|
46
46
|
|
|
47
47
|
async function migrateForward(configPath, inventoryPath, options) {
|
|
48
48
|
const inputBytes = await readFile(configPath);
|
|
49
|
+
if (options.expectedInputSha256 !== undefined && sha256(inputBytes) !== options.expectedInputSha256) {
|
|
50
|
+
throw new Error("Config changed after migration preview; no files were changed.");
|
|
51
|
+
}
|
|
49
52
|
const inputText = inputBytes.toString("utf8");
|
|
50
53
|
const { document, parsed } = parseMigrationConfig(inputText);
|
|
51
54
|
const version = parsed.version ?? 1;
|
|
@@ -85,6 +88,7 @@ async function migrateForward(configPath, inventoryPath, options) {
|
|
|
85
88
|
changed: true,
|
|
86
89
|
backup: basename(backup.configBackupPath),
|
|
87
90
|
manifest: basename(backup.manifestPath),
|
|
91
|
+
inventory_backup: backup.inventoryBackupPath === null ? null : basename(backup.inventoryBackupPath),
|
|
88
92
|
...report
|
|
89
93
|
};
|
|
90
94
|
} catch (error) {
|
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) {
|