agent-skillboard 0.2.7 → 0.2.9

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.
@@ -35,7 +35,7 @@ export function resolveVariantSnapshotFile({ configPath, snapshotPath }) {
35
35
  throw new Error(`snapshot path must stay under ${SNAPSHOT_ROOT}`);
36
36
  }
37
37
  return {
38
- storedPath: relative(configDir, absolutePath).replaceAll("\\", "/"),
38
+ storedPath: relative(configDir, absolutePath).replace(/\\/g, "/"),
39
39
  absolutePath,
40
40
  rootPath: root
41
41
  };
@@ -213,7 +213,7 @@ function normalizeStoredSnapshotPath(value) {
213
213
  if (value.includes("\0")) {
214
214
  throw new Error("snapshot path must not contain null bytes");
215
215
  }
216
- const normalized = value.replaceAll("\\", "/");
216
+ const normalized = value.replace(/\\/g, "/");
217
217
  if (normalized.startsWith("/") || normalized.startsWith("//") || /^[A-Za-z]:\//.test(normalized)) {
218
218
  throw new Error("snapshot path must be relative to the config directory");
219
219
  }
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;
package/src/hook-plan.mjs CHANGED
@@ -143,7 +143,7 @@ function modeToPermissions(mode) {
143
143
  }
144
144
 
145
145
  function shellQuote(value) {
146
- return `'${value.replaceAll("'", "'\\''")}'`;
146
+ return `'${value.replace(/'/g, "'\\''")}'`;
147
147
  }
148
148
 
149
149
  function safeHookFilePart(value) {
@@ -315,9 +315,9 @@ function normalizePath(value, root) {
315
315
 
316
316
  function displayPath(path, root) {
317
317
  if (!isAbsolute(path)) {
318
- return path.replaceAll("\\", "/");
318
+ return path.replace(/\\/g, "/");
319
319
  }
320
- const rel = relative(root, path).replaceAll("\\", "/");
320
+ const rel = relative(root, path).replace(/\\/g, "/");
321
321
  return rel.startsWith("..") ? path : rel;
322
322
  }
323
323
 
@@ -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";
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,40 @@ 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 question(rl, "Install SkillBoard agent integration now? [y/N] ");
184
+ return /^(y|yes)$/i.test(answer.trim());
185
+ } finally {
186
+ rl.close();
187
+ }
188
+ }
189
+
190
+ function question(rl, prompt) {
191
+ return new Promise((resolve) => {
192
+ rl.question(prompt, resolve);
193
+ });
194
+ }
195
+
123
196
  function formatList(values) {
124
197
  return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
125
198
  }
@@ -143,9 +216,96 @@ function defaultRuntime() {
143
216
  };
144
217
  }
145
218
 
219
+ async function agentSetupTargets(options, runtime) {
220
+ const env = runtime.env ?? process.env;
221
+ const home = env.HOME ?? env.USERPROFILE;
222
+ const requested = readCsv(options.get("agent"));
223
+ const supported = new Set(supportedAgentNames());
224
+ const names = requested.length === 0 ? supportedAgentNames() : requested;
225
+ const targets = [];
226
+ for (const name of names) {
227
+ if (!supported.has(name)) {
228
+ throw new Error(`Unsupported setup agent: ${name}`);
229
+ }
230
+ targets.push(...await setupAgentSkillTargets(name, home, env, { includeFallback: requested.length > 0 }));
231
+ }
232
+ return targets;
233
+ }
234
+
235
+ async function installAgentIntegration(targets) {
236
+ const created = [];
237
+ const updated = [];
238
+ const unchanged = [];
239
+ const preserved = [];
240
+ const content = agentIntegrationSkill();
241
+ for (const target of targets) {
242
+ const existing = await readFile(target.skillPath, "utf8").catch((error) => {
243
+ if (error?.code === "ENOENT") {
244
+ return null;
245
+ }
246
+ throw error;
247
+ });
248
+ if (existing === content) {
249
+ unchanged.push(`${target.agent}:${target.skillPath}`);
250
+ continue;
251
+ }
252
+ if (existing !== null && !existing.includes(AGENT_INTEGRATION_START)) {
253
+ preserved.push(`${target.agent}:${target.skillPath}`);
254
+ continue;
255
+ }
256
+ await mkdir(dirname(target.skillPath), { recursive: true });
257
+ await writeFile(target.skillPath, content, "utf8");
258
+ (existing === null ? created : updated).push(`${target.agent}:${target.skillPath}`);
259
+ }
260
+ return { created, updated, unchanged, preserved };
261
+ }
262
+
263
+ function agentIntegrationSkill() {
264
+ return `---
265
+ name: skillboard
266
+ 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.
267
+ ---
268
+ ${AGENT_INTEGRATION_START}
269
+ # SkillBoard Agent Integration
270
+
271
+ Use this skill to let SkillBoard guide skill selection above individual projects.
272
+
273
+ ## Layering
274
+
275
+ - SkillBoard is the user-level control plane for skill priority, overlap resolution, and workflow-aware routing.
276
+ - Project management belongs to the agent or workspace layer. Do not initialize, attach, rewrite, or manage a project just because SkillBoard is installed.
277
+ - Package install and \`skillboard setup\` install user-agent guidance only.
278
+
279
+ ## Default Behavior
280
+
281
+ - Installed user skills are usable by default unless the runtime, user, or local instructions disable them.
282
+ - Do not ask for permission merely because you selected a skill.
283
+ - When you use a skill, disclose it briefly at the start and completion.
284
+ - Use SkillBoard when more than one skill matches, when skills overlap, or when workflow priority should choose between plausible skills.
285
+
286
+ ## Cross-Agent Skill Reuse
287
+
288
+ - When the user wants to use a skill from another agent, run \`skillboard import-skill --from <source-agent> --to <this-agent> --skill <skill> --json\`.
289
+ - If SkillBoard reports the skill is compatible, install it with \`--yes\` and use the copied target-agent skill.
290
+ - If SkillBoard reports \`needs-adaptation\`, explain the compatibility reasons and ask before changing the skill body for this agent.
291
+ - 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\`.
292
+ - This is still user-agent setup. Do not create or mutate project policy files for cross-agent skill reuse.
293
+
294
+ ## Ambiguity Resolution
295
+
296
+ 1. Identify the candidate skills that match the request.
297
+ 2. Prefer the skill whose description, workflow guidance, and local instructions most directly match the user's task.
298
+ 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.
299
+ 4. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
300
+ 5. Continue with the selected skill; do not stop only because other candidate skills exist.
301
+
302
+ ${AGENT_INTEGRATION_END}
303
+ `;
304
+ }
305
+
146
306
  function commandPrefix(runtime) {
147
307
  const entrypoint = runtime.entrypointPath ?? "";
148
- const normalized = entrypoint.replaceAll("\\", "/");
308
+ const normalized = entrypoint.replace(/\\/g, "/");
149
309
  if (normalized.includes("/_npx/")) {
150
310
  const packageSpec = runtime.packageSpec ?? "agent-skillboard";
151
311
  return `npx --yes --package ${shellQuote(packageSpec)} skillboard`;
@@ -160,7 +320,7 @@ function isSourceTreeEntrypoint(entrypoint) {
160
320
  if (entrypoint === "") {
161
321
  return false;
162
322
  }
163
- const normalized = entrypoint.replaceAll("\\", "/");
323
+ const normalized = entrypoint.replace(/\\/g, "/");
164
324
  return (normalized === "bin/skillboard.mjs" || normalized.endsWith("/bin/skillboard.mjs"))
165
325
  && !normalized.includes("/node_modules/")
166
326
  && !normalized.includes("/_npx/")
@@ -169,7 +329,7 @@ function isSourceTreeEntrypoint(entrypoint) {
169
329
 
170
330
  function sourceTreeEntrypoint(entrypoint, cwd) {
171
331
  const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
172
- const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replaceAll("\\", "/");
332
+ const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
173
333
  if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
174
334
  return shellQuote(relativeEntrypoint);
175
335
  }
@@ -180,5 +340,5 @@ function shellQuote(value) {
180
340
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
181
341
  return value;
182
342
  }
183
- return `'${value.replaceAll("'", "'\\''")}'`;
343
+ return `'${value.replace(/'/g, "'\\''")}'`;
184
344
  }
@@ -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.
@@ -5,7 +5,7 @@ export function normalizeSkillPath(value, label = "skill path") {
5
5
  if (value.includes("\0")) {
6
6
  throw new Error(`${label} must not contain null bytes`);
7
7
  }
8
- const normalized = value.replaceAll("\\", "/");
8
+ const normalized = value.replace(/\\/g, "/");
9
9
  if (normalized.startsWith("/") || normalized.startsWith("//") || /^[A-Za-z]:\//.test(normalized)) {
10
10
  throw new Error(`${label} must be relative to the skills root`);
11
11
  }
@@ -164,7 +164,7 @@ function resolveUnderRoot(root, path) {
164
164
  }
165
165
 
166
166
  function relativePath(root, path) {
167
- const rel = relative(root, path).replaceAll("\\", "/");
167
+ const rel = relative(root, path).replace(/\\/g, "/");
168
168
  return rel.startsWith("..") ? path : rel;
169
169
  }
170
170
 
@@ -14,7 +14,7 @@ export async function importSource(options) {
14
14
  const matchedFiles = files
15
15
  .map((file) => ({
16
16
  file,
17
- relativeFile: relative(sourceRoot, file).replaceAll("\\", "/")
17
+ relativeFile: relative(sourceRoot, file).replace(/\\/g, "/")
18
18
  }))
19
19
  .filter((entry) => matchesAnyProfilePattern(entry.relativeFile, profile.skillPaths))
20
20
  .sort((left, right) => left.relativeFile.localeCompare(right.relativeFile));
@@ -238,9 +238,9 @@ function matchingPathRule(file, profile) {
238
238
  function patternToRegExp(pattern) {
239
239
  const escaped = pattern
240
240
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
241
- .replaceAll("**", "\0")
242
- .replaceAll("*", "[^/]*")
243
- .replaceAll("\0", ".*");
241
+ .split("**").join("\0")
242
+ .split("*").join("[^/]*")
243
+ .split("\0").join(".*");
244
244
  return new RegExp(`^${escaped}$`);
245
245
  }
246
246
 
@@ -165,7 +165,7 @@ export async function sourceDigest(path) {
165
165
 
166
166
  async function addPathDigest(hash, root, path) {
167
167
  const stats = await lstat(path);
168
- const rel = relative(root, path).replaceAll("\\", "/") || ".";
168
+ const rel = relative(root, path).replace(/\\/g, "/") || ".";
169
169
  if (stats.isSymbolicLink()) {
170
170
  hash.update(`symlink\0${rel}\0${await readlink(path)}\n`);
171
171
  return;
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";
package/src/workspace.mjs CHANGED
@@ -51,7 +51,7 @@ async function discoverInstalledSkills(skillsRoot, declaredSkills, options = {})
51
51
  const skillFiles = await findSkillFiles(skillsRoot);
52
52
  for (const file of skillFiles) {
53
53
  const frontmatter = parseSkillFrontmatter(await readFile(file, "utf8"));
54
- const path = relative(skillsRoot, file).replaceAll("\\", "/").replace(/\/SKILL\.md$/, "");
54
+ const path = relative(skillsRoot, file).replace(/\\/g, "/").replace(/\/SKILL\.md$/, "");
55
55
  const declared = declaredSkills.find((skill) => skill.path === path);
56
56
  appendInstalledSkill(installed, installedKeys, {
57
57
  id: declared?.id ?? frontmatter.name ?? path,