agent-skillboard 0.2.6 → 0.2.8

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.
@@ -1,4 +1,4 @@
1
- import YAML from "yaml";
1
+ // allow: SIZE_OK - control command split is deferred from the 0.2.7 release gate.
2
2
  import {
3
3
  EXPOSURE_VALUES,
4
4
  INVOCATION_VALUES,
@@ -28,6 +28,7 @@ import {
28
28
  import { canUseSkill } from "./can-use-guard.mjs";
29
29
 
30
30
  const WRITABLE_INVOCATIONS = new Set(["manual-only", "router-only", "workflow-auto", "global-auto"]);
31
+ const NON_ACTIVATABLE_STATUSES = new Set(["blocked", "deprecated", "archived", "removed"]);
31
32
 
32
33
  export async function activateSkill(options) {
33
34
  const { document, originalText } = await loadConfig(options.configPath);
@@ -38,6 +39,7 @@ export async function activateSkill(options) {
38
39
  if (!WRITABLE_INVOCATIONS.has(mode) || mode === "global-auto") {
39
40
  throw new Error(`activate requires --mode manual-only, router-only, or workflow-auto; got ${mode}`);
40
41
  }
42
+ ensureCanActivateSkill(options.skillId, skill, mode);
41
43
  skill.set("status", "active");
42
44
  skill.set("invocation", mode);
43
45
  addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
@@ -193,6 +195,7 @@ export async function preferSkill(options) {
193
195
  if (capabilityDefinition === undefined) {
194
196
  throw new Error(`Unknown capability: ${options.capability}`);
195
197
  }
198
+ ensureCanPreferSkill(options.skillId, skill);
196
199
  const required = ensureRequiredCapability(workflow, options.capability, document);
197
200
  const previousPreferred = readMapString(required, "preferred", "");
198
201
  if (previousPreferred.length > 0 && previousPreferred !== options.skillId) {
@@ -202,17 +205,6 @@ export async function preferSkill(options) {
202
205
  removeValue(ensureSeq(required, "fallback", document), options.skillId);
203
206
  addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
204
207
  removeValue(ensureSeq(workflow, "blocked_skills", document), options.skillId);
205
- const status = readMapString(skill, "status", "vendor");
206
- const invocation = readMapString(skill, "invocation", "manual-only");
207
- if (status === "quarantined" || status === "blocked") {
208
- skill.set("status", "active");
209
- }
210
- if (invocation === "blocked" || invocation === "deprecated") {
211
- const requiredPolicy = readMapString(required, "policy", "");
212
- const defaultPolicy = YAML.isMap(capabilityDefinition) ? readMapString(capabilityDefinition, "default_policy", "manual-only") : "manual-only";
213
- skill.set("invocation", requiredPolicy.length > 0 ? requiredPolicy : defaultPolicy);
214
- }
215
-
216
208
  return await writeCheckedConfig(
217
209
  document,
218
210
  originalText,
@@ -221,6 +213,31 @@ export async function preferSkill(options) {
221
213
  );
222
214
  }
223
215
 
216
+ function ensureCanActivateSkill(skillId, skill, mode) {
217
+ const status = readMapString(skill, "status", "vendor");
218
+ const invocation = readMapString(skill, "invocation", "manual-only");
219
+ if (NON_ACTIVATABLE_STATUSES.has(status)) {
220
+ throw new Error(`Cannot activate non-callable skill ${skillId} with status: ${status}`);
221
+ }
222
+ if (status === "quarantined" && mode !== "manual-only") {
223
+ throw new Error(`Cannot activate quarantined skill ${skillId} as ${mode}; use manual-only after source review`);
224
+ }
225
+ if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(invocation) && !(status === "quarantined" && invocation === "blocked" && mode === "manual-only")) {
226
+ throw new Error(`Cannot activate non-callable skill ${skillId} with invocation: ${invocation}`);
227
+ }
228
+ }
229
+
230
+ function ensureCanPreferSkill(skillId, skill) {
231
+ const status = readMapString(skill, "status", "vendor");
232
+ const invocation = readMapString(skill, "invocation", "manual-only");
233
+ if (NON_CALLABLE_WORKFLOW_STATUSES.has(status)) {
234
+ throw new Error(`Cannot prefer non-callable skill ${skillId} with status: ${status}`);
235
+ }
236
+ if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(invocation)) {
237
+ throw new Error(`Cannot prefer non-callable skill ${skillId} with invocation: ${invocation}`);
238
+ }
239
+ }
240
+
224
241
  function requireConfigSkill(document, skillId) {
225
242
  const skills = requireMapAt(document, ["skills"], "skills");
226
243
  const skill = skills.get(skillId, true);
package/src/doctor.mjs CHANGED
@@ -45,7 +45,7 @@ export async function doctorProject(options = {}) {
45
45
  };
46
46
 
47
47
  if (!configExists) {
48
- return finalizeDoctor(base, ["run skillboard init"]);
48
+ return finalizeDoctor(base, ["run skillboard setup once per user/agent install if agents should use SkillBoard for skill priority"]);
49
49
  }
50
50
 
51
51
  let workspace;
@@ -1,7 +1,13 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
2
5
  import { initProject } from "./init.mjs";
3
6
  import { uninstallProject } from "./uninstall.mjs";
4
7
 
8
+ const AGENT_INTEGRATION_START = "<!-- BEGIN SKILLBOARD AGENT INTEGRATION -->";
9
+ const AGENT_INTEGRATION_END = "<!-- END SKILLBOARD AGENT INTEGRATION -->";
10
+
5
11
  export async function runInitCommand(options, stdout, runtime = defaultRuntime()) {
6
12
  const root = resolve(options.get("dir") ?? ".");
7
13
  const result = await initProject({
@@ -36,6 +42,39 @@ export async function runInitCommand(options, stdout, runtime = defaultRuntime()
36
42
  return 0;
37
43
  }
38
44
 
45
+ export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
46
+ if (options.get("dir") !== undefined) {
47
+ throw new Error("skillboard setup is agent-layer setup and does not accept --dir");
48
+ }
49
+ const targets = await agentSetupTargets(options, runtime);
50
+ if (targets.length === 0) {
51
+ stdout.write("No supported agent user skill roots were detected.\n");
52
+ stdout.write(`Create a supported agent home or pass --agent ${supportedAgentNames().join(",")} to choose targets.\n`);
53
+ return 1;
54
+ }
55
+ if (options.get("yes") !== "true") {
56
+ writeSetupConfirmation(stdout, targets, commandPrefix(runtime));
57
+ if (!canPrompt(runtime)) {
58
+ return 1;
59
+ }
60
+ const confirmed = await promptForSetup(runtime);
61
+ if (!confirmed) {
62
+ stdout.write("Skipped SkillBoard setup.\n");
63
+ return 1;
64
+ }
65
+ }
66
+ const result = await installAgentIntegration(targets);
67
+ stdout.write("SkillBoard agent integration installed.\n");
68
+ writeList(stdout, "Created", result.created);
69
+ writeList(stdout, "Updated", result.updated);
70
+ writeList(stdout, "Unchanged", result.unchanged);
71
+ writeList(stdout, "Preserved", result.preserved);
72
+ stdout.write("Next:\n");
73
+ stdout.write("- Restart or refresh agents that cache user skills.\n");
74
+ stdout.write('- Ask the agent in a workspace: "Which skill should you use for this?"\n');
75
+ return 0;
76
+ }
77
+
39
78
  export async function runUninstallCommand(options, stdout) {
40
79
  const removeConfig = options.get("remove-config") === "true";
41
80
  const resetConfig = options.get("reset-config") === "true";
@@ -85,15 +124,15 @@ function writeCountedList(stdout, label, values) {
85
124
  }
86
125
 
87
126
  function writeSafetyDefault(stdout, safety) {
88
- stdout.write("Safety default:\n");
89
- stdout.write("- Installed does not mean allowed.\n");
127
+ stdout.write("Skill selection default:\n");
128
+ stdout.write("- Installed user skills are usable unless runtime, user, or local instructions disable them.\n");
90
129
  if (safety.automatic === 0) {
91
130
  stdout.write("- No automatic model invocation was enabled.\n");
92
131
  } else {
93
132
  stdout.write(`- ${safety.automatic} automatic skills enabled by existing policy.\n`);
94
133
  }
95
- stdout.write("- Imported local skills are manual-only.\n");
96
- stdout.write("- Runtime/plugin/system skills require source review before manual activation.\n");
134
+ stdout.write("- Imported local skills are available on request in generated local policy.\n");
135
+ stdout.write("- Runtime/plugin/system skills require source review before automatic invocation.\n");
97
136
  stdout.write(`- ${safety.automatic} automatic skills enabled\n`);
98
137
  stdout.write(`- ${safety.manualOnly} manual-only skills available\n`);
99
138
  stdout.write(`- ${safety.routerOnly} router-only skills available\n`);
@@ -120,6 +159,34 @@ function writeNextCommands(stdout, next) {
120
159
  stdout.write(`- ${next.command} brief --workflow ${shellQuote(workflow)}${dir} --verbose\n`);
121
160
  }
122
161
 
162
+ function writeSetupConfirmation(stdout, targets, command) {
163
+ stdout.write("SkillBoard setup installs agent-layer integration, not project files.\n");
164
+ stdout.write("It writes a SkillBoard guidance skill into detected user agent skill roots so agents can resolve skill priority when choices overlap.\n");
165
+ stdout.write("Targets:\n");
166
+ for (const target of targets) {
167
+ stdout.write(`- ${target.agent}: ${target.skillPath}\n`);
168
+ }
169
+ stdout.write("Run with --yes to install agent-layer integration:\n");
170
+ stdout.write(`- ${command} setup --agent ${targets.map((target) => target.agent).join(",")} --yes\n`);
171
+ }
172
+
173
+ function canPrompt(runtime) {
174
+ return runtime.stdin?.isTTY === true && runtime.stdout?.isTTY === true;
175
+ }
176
+
177
+ async function promptForSetup(runtime) {
178
+ const rl = createInterface({
179
+ input: runtime.stdin,
180
+ output: runtime.stdout
181
+ });
182
+ try {
183
+ const answer = await rl.question("Install SkillBoard agent integration now? [y/N] ");
184
+ return /^(y|yes)$/i.test(answer.trim());
185
+ } finally {
186
+ rl.close();
187
+ }
188
+ }
189
+
123
190
  function formatList(values) {
124
191
  return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
125
192
  }
@@ -143,6 +210,93 @@ function defaultRuntime() {
143
210
  };
144
211
  }
145
212
 
213
+ async function agentSetupTargets(options, runtime) {
214
+ const env = runtime.env ?? process.env;
215
+ const home = env.HOME ?? env.USERPROFILE;
216
+ const requested = readCsv(options.get("agent"));
217
+ const supported = new Set(supportedAgentNames());
218
+ const names = requested.length === 0 ? supportedAgentNames() : requested;
219
+ const targets = [];
220
+ for (const name of names) {
221
+ if (!supported.has(name)) {
222
+ throw new Error(`Unsupported setup agent: ${name}`);
223
+ }
224
+ targets.push(...await setupAgentSkillTargets(name, home, env, { includeFallback: requested.length > 0 }));
225
+ }
226
+ return targets;
227
+ }
228
+
229
+ async function installAgentIntegration(targets) {
230
+ const created = [];
231
+ const updated = [];
232
+ const unchanged = [];
233
+ const preserved = [];
234
+ const content = agentIntegrationSkill();
235
+ for (const target of targets) {
236
+ const existing = await readFile(target.skillPath, "utf8").catch((error) => {
237
+ if (error?.code === "ENOENT") {
238
+ return null;
239
+ }
240
+ throw error;
241
+ });
242
+ if (existing === content) {
243
+ unchanged.push(`${target.agent}:${target.skillPath}`);
244
+ continue;
245
+ }
246
+ if (existing !== null && !existing.includes(AGENT_INTEGRATION_START)) {
247
+ preserved.push(`${target.agent}:${target.skillPath}`);
248
+ continue;
249
+ }
250
+ await mkdir(dirname(target.skillPath), { recursive: true });
251
+ await writeFile(target.skillPath, content, "utf8");
252
+ (existing === null ? created : updated).push(`${target.agent}:${target.skillPath}`);
253
+ }
254
+ return { created, updated, unchanged, preserved };
255
+ }
256
+
257
+ function agentIntegrationSkill() {
258
+ return `---
259
+ name: skillboard
260
+ description: Use SkillBoard when several installed skills could apply, a skill choice is ambiguous, or the user asks which skill should be used, preferred, avoided, reviewed, or prioritized for the current workflow.
261
+ ---
262
+ ${AGENT_INTEGRATION_START}
263
+ # SkillBoard Agent Integration
264
+
265
+ Use this skill to let SkillBoard guide skill selection above individual projects.
266
+
267
+ ## Layering
268
+
269
+ - SkillBoard is the user-level control plane for skill priority, overlap resolution, and workflow-aware routing.
270
+ - Project management belongs to the agent or workspace layer. Do not initialize, attach, rewrite, or manage a project just because SkillBoard is installed.
271
+ - Package install and \`skillboard setup\` install user-agent guidance only.
272
+
273
+ ## Default Behavior
274
+
275
+ - Installed user skills are usable by default unless the runtime, user, or local instructions disable them.
276
+ - Do not ask for permission merely because you selected a skill.
277
+ - When you use a skill, disclose it briefly at the start and completion.
278
+ - Use SkillBoard when more than one skill matches, when skills overlap, or when workflow priority should choose between plausible skills.
279
+
280
+ ## Cross-Agent Skill Reuse
281
+
282
+ - When the user wants to use a skill from another agent, run \`skillboard import-skill --from <source-agent> --to <this-agent> --skill <skill> --json\`.
283
+ - If SkillBoard reports the skill is compatible, install it with \`--yes\` and use the copied target-agent skill.
284
+ - If SkillBoard reports \`needs-adaptation\`, explain the compatibility reasons and ask before changing the skill body for this agent.
285
+ - If the user approves, inspect the source skill, write an adapted \`SKILL.md\` for this agent, then install it with \`skillboard import-skill --from <source-agent> --to <this-agent> --skill <skill> --adapted-file <adapted-skill.md> --yes --json\`.
286
+ - This is still user-agent setup. Do not create or mutate project policy files for cross-agent skill reuse.
287
+
288
+ ## Ambiguity Resolution
289
+
290
+ 1. Identify the candidate skills that match the request.
291
+ 2. Prefer the skill whose description, workflow guidance, and local instructions most directly match the user's task.
292
+ 3. If a project or agent has explicit SkillBoard policy, use \`skillboard brief --intent <request> --json\` or \`skillboard route <intent> --workflow <name> --json\` to break ties.
293
+ 4. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
294
+ 5. Continue with the selected skill; do not stop only because other candidate skills exist.
295
+
296
+ ${AGENT_INTEGRATION_END}
297
+ `;
298
+ }
299
+
146
300
  function commandPrefix(runtime) {
147
301
  const entrypoint = runtime.entrypointPath ?? "";
148
302
  const normalized = entrypoint.replaceAll("\\", "/");
@@ -31,7 +31,7 @@ Your responsibility is to answer skill availability questions from SkillBoard, t
31
31
 
32
32
  ## Availability
33
33
 
34
- - Use SkillBoard as the source of truth; installed \`SKILL.md\` files are not automatically callable.
34
+ - Use SkillBoard as the source of truth for project-local policy and workflow priority; installed \`SKILL.md\` files are candidates, not enough to resolve overlap by themselves.
35
35
  - Read the current brief before answering: \`skillboard brief --json --config skillboard.config.yaml --skills skills\`. If the workflow is known, include \`--workflow <name>\`; add \`--include-actions\` when the user wants you to mediate a change.
36
36
  - When the user asks which skill fits a task, read \`skillboard brief --intent <request> --json --config skillboard.config.yaml --skills skills\`. Include \`--workflow <name>\` when known. Read \`assistant_guidance.route\`; use \`recommended_skill\`, \`fallback_skills\`, \`route_candidates\`, \`post_use_policy_suggestion\`, and \`guard_command\` instead of guessing from raw skill text. Inspect \`route_candidates\` when several skills match so denied candidates and selected fallbacks are clear. If \`post_use_policy_suggestion\` is present, work first with the allowed routed skill, then ask after completion whether to remember the suggested policy. If no skill matches, ask a clarifying question before choosing a skill.
37
37
  - Treat the brief sections headed "What your AI can use now", "Needs your decision", and "Blocked for safety" as the availability summary; do not infer availability from \`SKILL.md\` bodies.
package/src/uninstall.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ // allow: SIZE_OK - uninstall lifecycle split is deferred from the 0.2.7 release gate.
1
2
  import { access, lstat, readFile, readdir, rm, rmdir, writeFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { BRIDGE_END, BRIDGE_START, defaultConfig, hookReadme, profileReadme } from "./lifecycle-content.mjs";