agent-skillboard 0.2.17 → 0.3.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +128 -260
  3. package/bin/postinstall.mjs +2 -2
  4. package/docs/adapters.md +37 -113
  5. package/docs/ai-skill-routing-goal.md +35 -109
  6. package/docs/capabilities.md +17 -104
  7. package/docs/install.md +39 -493
  8. package/docs/policy-model.md +50 -280
  9. package/docs/positioning.md +19 -86
  10. package/docs/profiles.md +21 -153
  11. package/docs/reference.md +117 -356
  12. package/docs/rollout-runbook.md +23 -25
  13. package/docs/routing.md +23 -90
  14. package/docs/user-flow.md +60 -292
  15. package/docs/value-proof.md +23 -181
  16. package/docs/variant-lifecycle.md +47 -67
  17. package/docs/versioning.md +31 -264
  18. package/examples/v2-multi-source.config.yaml +35 -0
  19. package/examples/v2-policy-error.config.yaml +6 -0
  20. package/package.json +1 -1
  21. package/src/advisor/actions.mjs +102 -6
  22. package/src/advisor/application-commands.mjs +10 -9
  23. package/src/advisor/apply-action.mjs +74 -1
  24. package/src/advisor/guidance.mjs +24 -16
  25. package/src/advisor/schema.mjs +17 -6
  26. package/src/advisor/skills.mjs +18 -5
  27. package/src/advisor.mjs +27 -9
  28. package/src/agent-integration-cli.mjs +13 -4
  29. package/src/agent-integration-content.mjs +22 -12
  30. package/src/agent-integration-files.mjs +1 -1
  31. package/src/agent-inventory-platforms.mjs +10 -0
  32. package/src/agent-inventory.mjs +23 -1
  33. package/src/agent-skill-import.mjs +2 -2
  34. package/src/audit-paths.mjs +42 -0
  35. package/src/brief-cli.mjs +3 -2
  36. package/src/brief-renderer.mjs +1 -0
  37. package/src/cli.mjs +398 -127
  38. package/src/compatibility.mjs +24 -0
  39. package/src/control/can-use-guard.mjs +21 -1
  40. package/src/control/config-write.mjs +32 -2
  41. package/src/control/skill-crud.mjs +5 -0
  42. package/src/control/v2-guard.mjs +175 -0
  43. package/src/control/v2-skill-crud.mjs +32 -0
  44. package/src/control/v2-skill-forget.mjs +38 -0
  45. package/src/control/variant-status.mjs +47 -1
  46. package/src/control.mjs +55 -0
  47. package/src/doctor.mjs +65 -6
  48. package/src/domain/v2-policy.mjs +111 -0
  49. package/src/hook-plan.mjs +33 -3
  50. package/src/impact.mjs +52 -29
  51. package/src/index.mjs +25 -1
  52. package/src/init.mjs +50 -34
  53. package/src/inventory-install-units.mjs +63 -0
  54. package/src/inventory-json.mjs +279 -0
  55. package/src/inventory-refresh.mjs +163 -18
  56. package/src/lifecycle-cli.mjs +40 -12
  57. package/src/lifecycle-content.mjs +52 -67
  58. package/src/migration/v1-to-v2.mjs +212 -0
  59. package/src/migration/v2-files.mjs +211 -0
  60. package/src/migration/v2-journal.mjs +169 -0
  61. package/src/migration/v2-projection.mjs +108 -0
  62. package/src/migration/v2-transaction.mjs +205 -0
  63. package/src/policy.mjs +3 -0
  64. package/src/reconcile.mjs +139 -111
  65. package/src/report.mjs +168 -148
  66. package/src/review.mjs +2 -0
  67. package/src/route-advisory.mjs +47 -2
  68. package/src/route-selection.mjs +38 -2
  69. package/src/route.mjs +62 -2
  70. package/src/shared-skill.mjs +301 -0
  71. package/src/source-digest.mjs +42 -0
  72. package/src/source-profiles.mjs +27 -0
  73. package/src/source-verification.mjs +32 -48
  74. package/src/uninstall.mjs +22 -0
  75. package/src/user-state-paths.mjs +19 -0
  76. package/src/user-uninstall.mjs +146 -0
  77. package/src/workspace.mjs +41 -1
package/src/cli.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  // allow: SIZE_OK - legacy CLI dispatcher split is deferred from the 0.2.7 release gate.
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, isAbsolute, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import YAML from "yaml";
7
+ import { buildGeneratedInventory, mergeGeneratedInventory } from "./inventory-json.mjs";
7
8
  import {
8
9
  activateSkill,
9
10
  addHarness,
@@ -19,6 +20,7 @@ import {
19
20
  doctorProject,
20
21
  explainSkill,
21
22
  forkSkillVariant,
23
+ forgetV2Skill,
22
24
  importSource,
23
25
  installGuardHook,
24
26
  impactDisable,
@@ -33,6 +35,9 @@ import {
33
35
  quarantineSkill,
34
36
  removeSkill,
35
37
  resetSkillVariant,
38
+ setV2SkillEnabled,
39
+ setV2SkillPreference,
40
+ setV2SkillShared,
36
41
  reconcileWorkspace,
37
42
  refreshAgentInventory,
38
43
  refreshSourcePins,
@@ -58,15 +63,22 @@ import { renderSkillBrief } from "./brief-renderer.mjs";
58
63
  import { planGuardHookInstall } from "./control.mjs";
59
64
  import { writeCheckedConfig } from "./control/config-write.mjs";
60
65
  import { runInitCommand, runSetupCommand, runUninstallCommand } from "./lifecycle-cli.mjs";
66
+ import { migrateV2 } from "./migration/v2-transaction.mjs";
67
+ import { atomicWrite, optionalRead, withConfigLock } from "./migration/v2-files.mjs";
68
+ import { assertCurrentProjectionVersion } from "./compatibility.mjs";
61
69
  import { renderRouteSectionLines } from "./route-renderer.mjs";
70
+ import { resolveUserStatePaths } from "./user-state-paths.mjs";
71
+ import { setSkillSharing } from "./shared-skill.mjs";
72
+ import { supportedAgentNames } from "./agent-skill-roots.mjs";
62
73
 
63
74
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
64
75
 
65
- const APPLY_ACTION_VALUE_OPTIONS = new Set(["workflow", "dir", "config", "skills", "out", "skillboard-bin"]);
76
+ const APPLY_ACTION_VALUE_OPTIONS = new Set(["agent", "workflow", "intent", "dir", "config", "skills", "out", "skillboard-bin"]);
66
77
  const COMMAND_USAGE = new Map([
78
+ ["skill", ["enable|disable <skill-id> [--dry-run] [--json]", "share|unshare <skill-id> [--dry-run] [--json]", "preference <skill-id> --intent <term>[,<term>] --priority <integer> [--dry-run] [--json]", "forget <skill-id> [--dry-run] [--json]"]],
67
79
  ["setup", ["setup [--yes] [--agent codex[,claude,opencode,hermes]]"]],
68
80
  ["import-skill", ["import-skill --from <agent> --to <agent> --skill <id-or-dir> [--target-skill <id-or-dir>] [--adapted-file <path>] [--dry-run] [--yes] [--replace] [--json]"]],
69
- ["uninstall", ["uninstall [--dir <path>] [--dry-run] [--keep-settings] [--purge] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs] [--agent-layer] [--agent codex[,claude,opencode,hermes]]"]],
81
+ ["uninstall", ["uninstall --user (--dry-run|--yes) [--json]", "uninstall [--dir <path>] [--dry-run] [--keep-settings] [--purge] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs] [--agent-layer] [--agent codex[,claude,opencode,hermes]]"]],
70
82
  [
71
83
  "inventory",
72
84
  [
@@ -77,21 +89,22 @@ const COMMAND_USAGE = new Map([
77
89
  ["sources", ["sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]"]],
78
90
  ["import", ["import --profile <id-or-path> --source-root <dir> [--profile-dirs a,b] [--out <path>]", "import --profile <id-or-path> --source-root <dir> --config <path> --merge [--replace] [--dry-run]"]],
79
91
  ["scan", ["scan --config <path>"]],
92
+ ["migrate", ["migrate v2 [--config <path>] [--skills <dir>] [--yes] [--rollback <backup>] [--json]"]],
80
93
  ["check", ["check --config <path> --skills <dir>"]],
81
- ["list", ["list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]"]],
94
+ ["list", ["list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]"]],
82
95
  ["explain", ["explain <skill-id> --config <path> --skills <dir> [--json]"]],
83
- ["can-use", ["can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]"]],
96
+ ["can-use", ["can-use <skill-id> --agent codex|claude|opencode|hermes [--json]"]],
84
97
  ["audit", ["audit sources --config <path> --skills <dir> [--verify] [--json]"]],
85
98
  ["rollout", ["rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]"]],
86
99
  ["hook", ["hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]"]],
87
100
  ["lock", ["lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]"]],
88
- ["review", ["review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]"]],
89
- ["add", ["add skill <skill-id> --path <relative-skill-path> --config <path> --skills <dir> [--status <status>] [--invocation <mode>] [--exposure <exposure>] [--category <name>] [--workflow <name>] [--dry-run] [--json]", "add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]", "add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]"]],
90
- ["variant", ["variant add <variant-id> --from <base-id> --capability <name> --workflow <name> --config <path> --skills <dir> [--path <relative-skill-path>] [--mode manual-only|router-only|workflow-auto] [--category <name>] [--owner-install-unit <unit-id>] [--dry-run] [--json]", "variant fork <variant-id> --from <base-id> --capability <name> --workflow <name> --path <relative-skill-path> --config <path> --skills <dir> [--adapted-for <label>] [--category <name>] [--owner-install-unit <unit-id>] [--dry-run] [--json]", "variant status <variant-id> --config <path> --skills <dir> [--json]", "variant approve <variant-id> --config <path> --skills <dir> [--mode manual-only|router-only|workflow-auto] [--dry-run] [--json]", "variant reset <variant-id> --to-base|--to-approved --config <path> --skills <dir> [--yes] [--dry-run] [--mode manual-only|router-only|workflow-auto] [--json]"]],
91
- ["activate", ["activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]"]],
92
- ["block", ["block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]"]],
93
- ["quarantine", ["quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]"]],
94
- ["prefer", ["prefer <skill-id> --workflow <name> --capability <name> --config <path> --skills <dir> [--dry-run] [--json]"]],
101
+ ["review", ["review is a v1 compatibility command; run migrate v2 before policy changes"]],
102
+ ["add", ["add is a v1 compatibility command; run migrate v2, then use skill enable|disable|share|unshare|preference"]],
103
+ ["variant", ["variant lifecycle is compatibility-only; keep relationships and checkpoints in content or inventory metadata"]],
104
+ ["activate", ["activate is a v1 compatibility command; run migrate v2, then use skill enable"]],
105
+ ["block", ["block is a v1 compatibility command; run migrate v2, then use skill disable"]],
106
+ ["quarantine", ["quarantine is a v1 compatibility command; run migrate v2, then use skill disable"]],
107
+ ["prefer", ["prefer is a v1 compatibility command; run migrate v2, then use skill preference"]],
95
108
  ["remove", ["remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]"]],
96
109
  ["dashboard", ["dashboard --config <path> --skills <dir> [--out <path>]"]],
97
110
  ["reconcile", ["reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]"]],
@@ -114,7 +127,7 @@ export async function main(argv, stdout, stderr, stdin = process.stdin) {
114
127
  async function run(argv, stdout, stderr, stdin) {
115
128
  const command = argv[0] ?? "help";
116
129
  const commandArgs = argv.slice(1);
117
- const options = parseOptions(commandArgs);
130
+ const options = parseOptions(commandArgs, command);
118
131
  const help = selectedHelpText(command, commandArgs, options);
119
132
  if (help !== null) {
120
133
  stdout.write(help);
@@ -144,18 +157,23 @@ async function run(argv, stdout, stderr, stdin) {
144
157
  return await importProfile(options, stdout);
145
158
  case "scan":
146
159
  return await scan(options, stdout);
160
+ case "migrate":
161
+ return await migrateCommand(argv.slice(1), options, stdout);
147
162
  case "check":
148
163
  return await check(options, stdout, stderr);
149
164
  case "doctor":
150
165
  case "status":
151
166
  return await doctor(options, stdout);
152
167
  case "brief":
168
+ assertKnownOptions("brief", options, new Set(["agent", "workflow", "intent", "dir", "config", "skills", "include-actions", "verbose", "json"]));
153
169
  return await runBriefCommand(options, stdout, {
154
170
  configPath: configPath(options),
155
171
  skillsRoot: skillsRoot(options)
156
172
  });
157
173
  case "apply-action":
158
174
  return await applyActionCommand(argv.slice(1), options, stdout, stderr);
175
+ case "skill":
176
+ return await skillPolicy(argv.slice(1), options, stdout);
159
177
  case "list":
160
178
  return await list(argv.slice(1), options, stdout);
161
179
  case "explain":
@@ -226,6 +244,7 @@ function selectedHelpText(command, args, options) {
226
244
  }
227
245
 
228
246
  async function importProfile(options, stdout) {
247
+ assertKnownOptions("import", options, new Set(["profile", "source-root", "profile-dirs", "merge", "out", "config", "skills", "replace", "dry-run", "json"]));
229
248
  const profileRef = options.get("profile");
230
249
  const sourceRoot = options.get("source-root");
231
250
  if (profileRef === undefined || sourceRoot === undefined) {
@@ -250,24 +269,61 @@ async function importProfile(options, stdout) {
250
269
 
251
270
  async function mergeImport(options, imported, stdout) {
252
271
  const path = configPath(options);
253
- const originalText = await readFile(path, "utf8");
254
- const merged = mergeImportFragment(originalText, imported, { replace: options.get("replace") === "true" });
255
- const document = YAML.parseDocument(merged.text);
256
- if (document.errors.length > 0) {
257
- throw new Error(`Invalid YAML config after import merge: ${document.errors.map((error) => error.message).join("; ")}`);
258
- }
259
- const checked = await writeCheckedConfig(document, originalText, {
260
- configPath: path,
261
- skillsRoot: skillsRoot(options),
262
- dryRun: options.get("dry-run") === "true"
263
- }, `Import merged: ${path}`);
264
- const result = {
265
- ...checked,
266
- addedSkills: merged.addedSkills,
267
- addedInstallUnits: merged.addedInstallUnits,
268
- replacedSkills: merged.replacedSkills,
269
- replacedInstallUnits: merged.replacedInstallUnits
270
- };
272
+ const result = await withConfigLock(path, async () => {
273
+ const originalBytes = await readFile(path);
274
+ const originalText = originalBytes.toString("utf8");
275
+ const originalMode = (await stat(path)).mode & 0o777;
276
+ const merged = mergeImportFragment(originalText, imported, { replace: options.get("replace") === "true" });
277
+ const document = YAML.parseDocument(merged.text);
278
+ if (document.errors.length > 0) {
279
+ throw new Error(`Invalid YAML config after import merge: ${document.errors.map((error) => error.message).join("; ")}`);
280
+ }
281
+ const inventoryPath = resolve(dirname(path), ".skillboard", "inventory.json");
282
+ const previousInventory = document.get("version") === 2 ? await optionalRead(inventoryPath) : null;
283
+ const previousInventoryMode = previousInventory === null ? null : (await stat(inventoryPath)).mode & 0o777;
284
+ let inventoryText = null;
285
+ if (document.get("version") === 2) {
286
+ const added = await buildGeneratedInventory({
287
+ skills: imported.skills,
288
+ installUnits: [imported.installUnit]
289
+ }, { root: dirname(path), home: process.env.HOME });
290
+ inventoryText = mergeGeneratedInventory(previousInventory?.toString("utf8") ?? null, added, {
291
+ replace: options.get("replace") === "true"
292
+ });
293
+ }
294
+ const checked = await writeCheckedConfig(document, originalText, {
295
+ configPath: path,
296
+ skillsRoot: skillsRoot(options),
297
+ dryRun: options.get("dry-run") === "true"
298
+ }, "Import merged");
299
+ if (inventoryText !== null && options.get("dry-run") !== "true") {
300
+ try {
301
+ await atomicWrite(inventoryPath, Buffer.from(inventoryText));
302
+ await chmod(inventoryPath, 0o600);
303
+ if (process.env.SKILLBOARD_IMPORT_FAILPOINT === "after-inventory-write") {
304
+ throw new Error("Injected import failure after inventory write.");
305
+ }
306
+ await loadWorkspace({ configPath: path, skillsRoot: skillsRoot(options) });
307
+ } catch (error) {
308
+ await atomicWrite(path, originalBytes);
309
+ await chmod(path, originalMode);
310
+ if (previousInventory === null) await rm(inventoryPath, { force: true });
311
+ else {
312
+ await atomicWrite(inventoryPath, previousInventory);
313
+ await chmod(inventoryPath, previousInventoryMode);
314
+ }
315
+ throw error;
316
+ }
317
+ }
318
+ return {
319
+ ...checked,
320
+ addedSkills: merged.addedSkills,
321
+ addedInstallUnits: merged.addedInstallUnits,
322
+ replacedSkills: merged.replacedSkills,
323
+ replacedInstallUnits: merged.replacedInstallUnits,
324
+ inventoryPath: inventoryText === null ? null : ".skillboard/inventory.json"
325
+ };
326
+ });
271
327
  writeOutput(stdout, result, options, () => renderImportMerge(result));
272
328
  return 0;
273
329
  }
@@ -296,11 +352,19 @@ async function inventory(argv, options, stdout) {
296
352
  if (args[0] !== "refresh") {
297
353
  throw new Error("Usage: skillboard inventory refresh|detect ...");
298
354
  }
355
+ const allowedOptions = new Set(["dir", "config", "scan-root", "dry-run", "json", "help"]);
356
+ const unknownOption = [...options.keys()].find((key) => !allowedOptions.has(key));
357
+ if (unknownOption !== undefined) {
358
+ throw new Error(`Unknown inventory refresh option: --${unknownOption}`);
359
+ }
360
+ const state = statePaths(options);
299
361
  const result = await refreshAgentInventory({
300
- root: options.get("dir") ?? ".",
301
- configPath: options.get("config"),
362
+ root: options.get("dir") ?? dirname(state.configPath),
363
+ configPath: state.configPath,
302
364
  roots: readCsv(options.get("scan-root")),
303
- dryRun: options.get("dry-run") === "true"
365
+ dryRun: options.get("dry-run") === "true",
366
+ home: state.home,
367
+ env: process.env
304
368
  });
305
369
  writeOutput(stdout, result, options, () => renderInventoryRefresh(result));
306
370
  return 0;
@@ -345,6 +409,7 @@ async function scan(options, stdout) {
345
409
  }
346
410
 
347
411
  async function check(options, stdout, stderr) {
412
+ assertKnownOptions("check", options, new Set(["config", "skills", "json"]));
348
413
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
349
414
  const result = checkPolicy(workspace);
350
415
  const output = [...result.errors, ...result.warnings].join("\n");
@@ -355,10 +420,36 @@ async function check(options, stdout, stderr) {
355
420
  return result.ok ? 0 : 1;
356
421
  }
357
422
 
423
+ async function migrateCommand(argv, options, stdout) {
424
+ const allowedOptions = new Set(["config", "skills", "yes", "rollback", "json", "help"]);
425
+ const unknownOption = [...options.keys()].find((key) => !allowedOptions.has(key));
426
+ if (unknownOption !== undefined) {
427
+ throw new Error(`Unknown migrate option: --${unknownOption}`);
428
+ }
429
+ const args = argv.filter((arg) => !arg.startsWith("--"));
430
+ if (args[0] !== "v2") {
431
+ throw new Error("Usage: skillboard migrate v2 [--config <path>] [--skills <dir>] [--yes] [--rollback <backup>] [--json]");
432
+ }
433
+ if (options.has("yes") && options.has("rollback")) {
434
+ throw new Error("Choose either --yes to apply migration or --rollback <backup>, not both.");
435
+ }
436
+ const result = await migrateV2({
437
+ configPath: configPath(options),
438
+ skillsRoot: skillsRoot(options),
439
+ apply: options.get("yes") === "true",
440
+ rollbackPath: options.get("rollback"),
441
+ failpoint: process.env.SKILLBOARD_MIGRATION_FAILPOINT
442
+ });
443
+ writeOutput(stdout, result, options, () => renderMigration(result));
444
+ return 0;
445
+ }
446
+
358
447
  async function doctor(options, stdout) {
448
+ assertKnownOptions("doctor", options, new Set(["dir", "config", "skills", "strict", "verify", "summary", "json"]));
449
+ const state = statePaths(options);
359
450
  const result = await doctorProject({
360
- root: options.get("dir") ?? ".",
361
- configPath: options.get("config"),
451
+ root: options.get("dir") ?? dirname(state.configPath),
452
+ configPath: state.configPath,
362
453
  skillsRoot: options.get("skills"),
363
454
  verifySources: options.get("verify") === "true"
364
455
  });
@@ -369,6 +460,9 @@ async function doctor(options, stdout) {
369
460
 
370
461
  async function applyActionCommand(argv, options, stdout, stderr) {
371
462
  try {
463
+ assertKnownOptions("apply-action", options, new Set([
464
+ "agent", "workflow", "intent", "dir", "config", "skills", "dry-run", "yes", "allow-destructive", "out", "skillboard-bin", "json"
465
+ ]));
372
466
  const actionIds = positionalArgs(argv, APPLY_ACTION_VALUE_OPTIONS);
373
467
  if (actionIds.length !== 1) {
374
468
  throw new ApplyActionError(
@@ -377,10 +471,16 @@ async function applyActionCommand(argv, options, stdout, stderr) {
377
471
  );
378
472
  }
379
473
  const [actionId] = actionIds;
474
+ const state = statePaths(options);
380
475
  const result = await applyAdvisorAction(actionId, {
381
476
  root: commandRoot(options),
477
+ agent: options.get("agent"),
382
478
  workflow: options.get("workflow"),
479
+ intent: options.get("intent"),
383
480
  configPath: configPath(options),
481
+ inventoryPath: state.inventoryPath,
482
+ home: state.home,
483
+ env: process.env,
384
484
  skillsRoot: skillsRoot(options),
385
485
  dryRun: options.get("dry-run") === "true",
386
486
  yes: options.get("yes") === "true",
@@ -401,7 +501,47 @@ async function applyActionCommand(argv, options, stdout, stderr) {
401
501
  }
402
502
  }
403
503
 
504
+ async function skillPolicy(argv, options, stdout) {
505
+ assertKnownOptions("skill", options, new Set(["config", "skills", "dry-run", "json", "intent", "priority"]));
506
+ const args = positionalArgs(argv);
507
+ const operation = args[0];
508
+ const skillId = args[1];
509
+ if (skillId === undefined) throw new Error("Usage: skillboard skill enable|disable|share|unshare|preference|forget <skill-id>");
510
+ const common = { skillId, configPath: configPath(options), skillsRoot: skillsRoot(options), dryRun: options.get("dry-run") === "true" };
511
+ let result;
512
+ if (operation === "enable" || operation === "disable") {
513
+ result = await setV2SkillEnabled({ ...common, enabled: operation === "enable" });
514
+ } else if (operation === "share" || operation === "unshare") {
515
+ const state = statePaths(options);
516
+ result = await setSkillSharing({
517
+ ...common,
518
+ shared: operation === "share",
519
+ configPath: state.configPath,
520
+ inventoryPath: state.inventoryPath,
521
+ home: state.home,
522
+ env: process.env
523
+ });
524
+ writeOutput(stdout, result, options, () => `${operation === "share" ? "Shared" : "Unshared"} ${skillId}\n`);
525
+ return 0;
526
+ } else if (operation === "preference") {
527
+ const priority = Number(options.get("priority"));
528
+ result = await setV2SkillPreference({ ...common, intents: readCsv(options.get("intent")), priority });
529
+ } else if (operation === "forget") {
530
+ const state = statePaths(options);
531
+ result = await forgetV2Skill({
532
+ ...common,
533
+ configPath: state.configPath,
534
+ inventoryPath: state.inventoryPath
535
+ });
536
+ } else {
537
+ throw new Error(`Unknown skill policy operation: ${operation}`);
538
+ }
539
+ writeControlResult(stdout, result, options);
540
+ return 0;
541
+ }
542
+
404
543
  async function list(argv, options, stdout) {
544
+ assertKnownOptions("list", options, new Set(["workflow", "config", "skills", "json"]));
405
545
  const kind = positionalArgs(argv)[0] ?? "skills";
406
546
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
407
547
  let result;
@@ -421,6 +561,7 @@ async function list(argv, options, stdout) {
421
561
  }
422
562
 
423
563
  async function explain(argv, options, stdout) {
564
+ assertKnownOptions("explain", options, new Set(["config", "skills", "json"]));
424
565
  const skillId = positionalArgs(argv)[0];
425
566
  if (skillId === undefined) {
426
567
  throw new Error("Usage: skillboard explain <skill-id>");
@@ -432,17 +573,23 @@ async function explain(argv, options, stdout) {
432
573
  }
433
574
 
434
575
  async function route(argv, options, stdout) {
576
+ assertKnownOptions("route", options, new Set(["agent", "workflow", "config", "skills", "json"]));
435
577
  const intent = positionalArgs(argv).join(" ").trim();
436
578
  const workflow = options.get("workflow");
437
- if (intent.length === 0 || workflow === undefined) {
438
- throw new Error("Usage: skillboard route <intent> --workflow <name>");
579
+ if (intent.length === 0) {
580
+ throw new Error("Usage: skillboard route <intent> [--agent <name>]");
439
581
  }
440
582
  const config = configPath(options);
441
583
  const skills = skillsRoot(options);
442
584
  const workspace = await loadWorkspace({ configPath: config, skillsRoot: skills });
585
+ assertV2AgentOption(workspace, options, "skillboard route <intent> --agent <name>");
586
+ if (workspace.version === 1 && workflow === undefined) {
587
+ throw new Error("Usage: skillboard route <intent> --workflow <name>");
588
+ }
443
589
  const result = routeSkill(workspace, {
444
590
  intent,
445
591
  workflow,
592
+ agent: options.get("agent"),
446
593
  configPath: config,
447
594
  skillsRoot: skills
448
595
  });
@@ -451,26 +598,44 @@ async function route(argv, options, stdout) {
451
598
  }
452
599
 
453
600
  async function canUse(argv, options, stdout) {
601
+ assertKnownOptions("can-use", options, new Set(["agent", "workflow", "config", "skills", "json"]));
454
602
  const skillId = positionalArgs(argv)[0];
455
603
  const workflow = options.get("workflow");
456
- if (skillId === undefined || workflow === undefined) {
457
- throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
604
+ if (skillId === undefined) {
605
+ throw new Error("Usage: skillboard can-use <skill-id> [--agent <name>]");
458
606
  }
459
607
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
460
- const result = canUseSkill(workspace, skillId, workflow);
608
+ assertV2AgentOption(workspace, options, "skillboard can-use <skill-id> --agent <name>");
609
+ if (workspace.version === 1 && workflow === undefined) {
610
+ throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
611
+ }
612
+ const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
461
613
  writeOutput(stdout, result, options, () => renderCanUse(result));
462
614
  return result.allowed ? 0 : 2;
463
615
  }
464
616
 
465
617
  async function guard(argv, options, stdout) {
618
+ assertKnownOptions("guard", options, new Set(["agent", "workflow", "config", "skills", "json", "hook-projection-version"]));
466
619
  const args = positionalArgs(argv);
467
620
  const skillId = args[0] === "use" ? args[1] : args[0];
468
621
  const workflow = options.get("workflow");
469
- if (skillId === undefined || workflow === undefined) {
470
- throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
622
+ if (skillId === undefined) {
623
+ throw new Error("Usage: skillboard guard use <skill-id> [--agent <name>]");
471
624
  }
472
625
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
473
- const result = canUseSkill(workspace, skillId, workflow);
626
+ const hookProjection = options.get("hook-projection-version") ?? process.env.SKILLBOARD_POLICY_PROJECTION_VERSION;
627
+ if (args[0] !== "use" && hookProjection === undefined) {
628
+ throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
629
+ }
630
+ if (process.env.SKILLBOARD_SKILL_ID !== undefined && hookProjection === undefined) {
631
+ throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
632
+ }
633
+ assertCurrentProjectionVersion(hookProjection, workspace.version);
634
+ assertV2AgentOption(workspace, options, "skillboard guard use <skill-id> --agent <name>");
635
+ if (workspace.version === 1 && workflow === undefined) {
636
+ throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
637
+ }
638
+ const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
474
639
  if (options.get("json") === "true") {
475
640
  stdout.write(`${JSON.stringify(result, null, 2)}\n`);
476
641
  } else {
@@ -480,6 +645,7 @@ async function guard(argv, options, stdout) {
480
645
  }
481
646
 
482
647
  async function audit(argv, options, stdout) {
648
+ assertKnownOptions("audit", options, new Set(["verify", "config", "skills", "json"]));
483
649
  const args = positionalArgs(argv);
484
650
  if ((args[0] ?? "sources") !== "sources") {
485
651
  throw new Error("Usage: skillboard audit sources --config <path> --skills <dir> [--json]");
@@ -520,6 +686,7 @@ async function rollout(argv, options, stdout) {
520
686
  }
521
687
 
522
688
  async function hook(argv, options, stdout) {
689
+ assertKnownOptions("hook", options, new Set(["workflow", "out", "skillboard-bin", "config", "skills", "dry-run", "json"]));
523
690
  const args = positionalArgs(argv);
524
691
  if (args[0] !== "install") {
525
692
  throw new Error("Usage: skillboard hook install --workflow <name> [--out <path>] [--skillboard-bin <path>] [--dry-run]");
@@ -704,6 +871,10 @@ async function addHarnessCommand(args, options, stdout) {
704
871
 
705
872
  async function variant(argv, options, stdout) {
706
873
  try {
874
+ assertKnownOptions("variant", options, new Set([
875
+ "from", "capability", "workflow", "path", "mode", "category", "owner-install-unit",
876
+ "adapted-for", "config", "skills", "to-base", "to-approved", "yes", "dry-run", "json"
877
+ ]));
707
878
  return await runVariant(argv, options, stdout);
708
879
  } catch (error) {
709
880
  const payload = variantErrorPayload(error);
@@ -718,6 +889,9 @@ async function variant(argv, options, stdout) {
718
889
  async function runVariant(argv, options, stdout) {
719
890
  const args = positionalArgs(argv);
720
891
  const subcommand = args[0];
892
+ if (options.get("config") !== undefined && ["add", "fork", "status", "approve", "reset"].includes(subcommand)) {
893
+ await assertVariantPolicyBoundary(configPath(options), subcommand);
894
+ }
721
895
  if (subcommand === "add") {
722
896
  return await variantAddCommand(args, options, stdout);
723
897
  }
@@ -736,6 +910,20 @@ async function runVariant(argv, options, stdout) {
736
910
  throw new VariantCliError("unknown_variant_subcommand", `Unknown variant subcommand: ${subcommand ?? "<missing>"}`, 2);
737
911
  }
738
912
 
913
+ async function assertVariantPolicyBoundary(path, subcommand) {
914
+ const document = YAML.parseDocument(await readFile(path, "utf8"));
915
+ if (document.errors.length > 0) {
916
+ throw new Error(`Invalid YAML config: ${document.errors.map((error) => error.message).join("; ")}`);
917
+ }
918
+ if (document.get("version") === 2 && subcommand !== "status") {
919
+ throw new VariantCliError(
920
+ "compatibility_error",
921
+ "Variant policy mutation is unavailable for version 2; use skill enable, disable, share, unshare, or preference. Read-only variant status remains available.",
922
+ 1
923
+ );
924
+ }
925
+ }
926
+
739
927
  async function variantAddCommand(args, options, stdout) {
740
928
  const variantId = args[1];
741
929
  const baseId = options.get("from");
@@ -983,6 +1171,7 @@ async function remove(argv, options, stdout) {
983
1171
  }
984
1172
 
985
1173
  async function dashboard(options, stdout) {
1174
+ assertKnownOptions("dashboard", options, new Set(["config", "skills", "out", "json"]));
986
1175
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
987
1176
  const markdown = renderDashboard(workspace);
988
1177
  const out = options.get("out");
@@ -997,6 +1186,7 @@ async function dashboard(options, stdout) {
997
1186
  }
998
1187
 
999
1188
  async function reconcile(options, stdout) {
1189
+ assertKnownOptions("reconcile", options, new Set(["config", "skills", "actual-harnesses", "out", "json"]));
1000
1190
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1001
1191
  const plan = reconcileWorkspace(workspace, { actualHarnesses: readCsv(options.get("actual-harnesses")) });
1002
1192
  const markdown = renderReconcilePlan(plan);
@@ -1012,6 +1202,7 @@ async function reconcile(options, stdout) {
1012
1202
  }
1013
1203
 
1014
1204
  async function impact(argv, options, stdout) {
1205
+ assertKnownOptions("impact", options, new Set(["config", "skills", "out", "json"]));
1015
1206
  if (argv[0] !== "disable" || argv[1] === undefined) {
1016
1207
  throw new Error("Usage: skillboard impact disable <skill-id>");
1017
1208
  }
@@ -1058,23 +1249,47 @@ function formatActiveConflicts(entries) {
1058
1249
  }
1059
1250
 
1060
1251
  function configPath(options) {
1061
- return options.get("config") ?? "skillboard.config.yaml";
1252
+ return statePaths(options).configPath;
1062
1253
  }
1063
1254
 
1064
1255
  function skillsRoot(options) {
1065
- return options.get("skills") ?? "skills";
1256
+ return options.get("skills");
1066
1257
  }
1067
1258
 
1068
1259
  function commandRoot(options) {
1260
+ return options.get("dir") ?? dirname(configPath(options));
1261
+ }
1262
+
1263
+ function statePaths(options) {
1069
1264
  const dir = options.get("dir");
1070
- if (dir !== undefined) {
1071
- return dir;
1265
+ return resolveUserStatePaths({
1266
+ env: process.env,
1267
+ cwd: process.cwd(),
1268
+ configPath: options.get("config") ?? (dir === undefined ? undefined : resolve(dir, "skillboard.config.yaml"))
1269
+ });
1270
+ }
1271
+
1272
+ function assertV2AgentOption(workspace, options, usage) {
1273
+ if (workspace.version !== 2) return;
1274
+ const agent = options.get("agent");
1275
+ if (agent === undefined) {
1276
+ throw new Error(`Version 2 availability requires --agent. Usage: ${usage}`);
1277
+ }
1278
+ if (!supportedAgentNames().includes(agent)) {
1279
+ throw new Error(`Unsupported agent: ${agent}. Expected one of: ${supportedAgentNames().join(", ")}.`);
1072
1280
  }
1073
- const config = options.get("config");
1074
- return config !== undefined && isAbsolute(config) ? dirname(config) : ".";
1075
1281
  }
1076
1282
 
1077
- function parseOptions(args) {
1283
+ const VALUE_OPTIONS = new Set([
1284
+ "actual-harnesses", "adapted-file", "adapted-for", "agent", "cache-dir", "capability", "category",
1285
+ "command", "config", "config-file", "dir", "exposure", "from", "harness", "harness-status",
1286
+ "hook-projection-version", "install-output", "intent", "invocation", "kind", "mode", "out",
1287
+ "owner-install-unit", "path", "priority", "profile", "profile-dirs", "rollback", "rollouts-dir",
1288
+ "scan-root", "scope", "skill", "skillboard-bin", "skills", "source", "source-root", "status",
1289
+ "target-skill", "to", "transaction", "trust-level", "unit", "workflow"
1290
+ ]);
1291
+
1292
+ function parseOptions(args, command) {
1078
1293
  const options = new Map();
1079
1294
  for (let index = 0; index < args.length; index += 1) {
1080
1295
  const arg = args[index];
@@ -1084,6 +1299,7 @@ function parseOptions(args) {
1084
1299
  const key = arg.slice(2);
1085
1300
  const value = args[index + 1];
1086
1301
  if (value === undefined || value.startsWith("--")) {
1302
+ if (VALUE_OPTIONS.has(key)) throw new Error(`Missing value for ${command} option: --${key}`);
1087
1303
  options.set(key, "true");
1088
1304
  continue;
1089
1305
  }
@@ -1093,6 +1309,14 @@ function parseOptions(args) {
1093
1309
  return options;
1094
1310
  }
1095
1311
 
1312
+ function assertKnownOptions(command, options, allowed) {
1313
+ for (const option of options.keys()) {
1314
+ if (!allowed.has(option)) {
1315
+ throw new Error(`Unknown ${command} option: --${option}`);
1316
+ }
1317
+ }
1318
+ }
1319
+
1096
1320
  function formatList(values) {
1097
1321
  return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
1098
1322
  }
@@ -1124,8 +1348,9 @@ function renderImportMerge(result) {
1124
1348
  }
1125
1349
 
1126
1350
  function renderInventoryRefresh(result) {
1127
- return [
1351
+ const lines = [
1128
1352
  `${result.dryRun ? "Dry run: " : ""}Inventory refreshed: ${result.configPath}`,
1353
+ ...(result.bootstrappedV2 ? ["Bootstrapped minimal SkillBoard v2 policy."] : []),
1129
1354
  renderChangePlan(result.plan).trimEnd(),
1130
1355
  `Scanned skills: ${result.scan.scannedSkills}`,
1131
1356
  `Scanned install units: ${result.scan.scannedInstallUnits}`,
@@ -1138,7 +1363,8 @@ function renderInventoryRefresh(result) {
1138
1363
  `Review notes: ${formatList(result.scan.reviewNotes ?? [])}`,
1139
1364
  `Scan warnings: ${formatList(result.scan.warnings ?? [])}`,
1140
1365
  ""
1141
- ].join("\n");
1366
+ ];
1367
+ return lines.join("\n");
1142
1368
  }
1143
1369
 
1144
1370
  function renderInstallDetection(result) {
@@ -1303,6 +1529,10 @@ function renderList(kind, values) {
1303
1529
  }
1304
1530
  if (kind === "skills") {
1305
1531
  return `${values.map((skill) => {
1532
+ if (typeof skill.enabled === "boolean") {
1533
+ const inventory = skill.inventory ?? { path: skill.path ?? null, owner_install_unit: null };
1534
+ return `${skill.id}\tenabled=${skill.enabled}\tshared=${skill.shared}\tagents=${(inventory.installed_on ?? []).join(",") || "none"}\tpath=${inventory.path ?? "none"}\towner=${inventory.owner_install_unit ?? "none"}`;
1535
+ }
1306
1536
  const roles = skill.workflowRoles.length === 0 ? "none" : skill.workflowRoles.join(",");
1307
1537
  const owner = skill.ownerInstallUnit ?? "direct";
1308
1538
  return `${skill.id}\t${skill.status}\t${skill.invocation}\t${skill.sourceClass}\towner=${owner}\troles=${roles}`;
@@ -1324,6 +1554,18 @@ function renderList(kind, values) {
1324
1554
  }
1325
1555
 
1326
1556
  function renderExplain(result) {
1557
+ if (typeof result.enabled === "boolean") {
1558
+ const inventory = result.inventory ?? { path: result.path ?? null, owner_install_unit: null };
1559
+ return [
1560
+ `Skill: ${result.id}`,
1561
+ `Enabled: ${result.enabled}`,
1562
+ `Shared: ${result.shared}`,
1563
+ `Installed on: ${(inventory.installed_on ?? []).join(", ") || "none"}`,
1564
+ `Inventory path: ${inventory.path ?? "none"}`,
1565
+ `Inventory owner: ${inventory.owner_install_unit ?? "none"}`,
1566
+ ""
1567
+ ].join("\n");
1568
+ }
1327
1569
  const workflows = result.workflows.length === 0
1328
1570
  ? "none"
1329
1571
  : result.workflows.map((workflow) => `${workflow.workflow}:${workflow.roles.join(",")}`).join(", ");
@@ -1379,6 +1621,24 @@ function renderRoute(result) {
1379
1621
  return lines.join("\n");
1380
1622
  }
1381
1623
 
1624
+ function renderMigration(result) {
1625
+ const lines = [
1626
+ `SkillBoard v2 migration: ${result.mode}`,
1627
+ `Changed: ${result.changed}`,
1628
+ `Target version: ${result.target_version}`,
1629
+ `Input SHA-256: ${result.input_sha256}`,
1630
+ `Config SHA-256: ${result.config_sha256}`
1631
+ ];
1632
+ if (result.inventory_sha256 !== null) lines.push(`Inventory SHA-256: ${result.inventory_sha256}`);
1633
+ if (result.backup !== null) lines.push(`Backup: ${result.backup}`);
1634
+ if (result.counts !== undefined) {
1635
+ lines.push(`Skills: ${result.counts.skills} (${result.counts.enabled} enabled, ${result.counts.disabled} disabled)`);
1636
+ lines.push(`Warnings: ${result.counts.warnings}; losses: ${result.counts.losses}`);
1637
+ }
1638
+ lines.push("");
1639
+ return lines.join("\n");
1640
+ }
1641
+
1382
1642
  function renderSourceAudit(result) {
1383
1643
  const lines = [
1384
1644
  `Source audit: ${result.ok ? "passed" : "failed"}`,
@@ -1511,65 +1771,52 @@ function helpText() {
1511
1771
  " The package postinstall auto-runs agent-layer guidance setup on install and update.",
1512
1772
  " Under sudo, setup targets SUDO_USER's agent homes while npm still controls the binary prefix.",
1513
1773
  " Run skillboard setup later after adding another supported agent or when install scripts were skipped.",
1514
- " Run skillboard uninstall --agent-layer before package removal when managed agent guidance should disappear.",
1774
+ " Preview skillboard uninstall --user --dry-run before package removal to clean every managed artifact.",
1515
1775
  "",
1516
- "AI/automation operations:",
1776
+ "Core AI/automation operations:",
1517
1777
  " setup [--yes] [--agent codex[,claude,opencode,hermes]]",
1518
1778
  " import-skill --from <agent> --to <agent> --skill <id-or-dir> [--target-skill <id-or-dir>] [--adapted-file <path>] [--dry-run] [--yes] [--replace] [--json]",
1519
- " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1520
- " uninstall [--dir <path>] [--dry-run] [--keep-settings] [--purge] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs] [--agent-layer] [--agent codex[,claude,opencode,hermes]]",
1779
+ " uninstall --user (--dry-run|--yes) [--json]",
1521
1780
  " inventory refresh [--dir <path>] [--config <path>] [--scan-root <dir>[,<dir>]] [--dry-run] [--json]",
1522
- " inventory detect --unit <id> --config <path> [--install-output <path>] [--config-file a,b] [--source <value>] [--kind <kind>] [--scope <scope>] [--dry-run] [--json]",
1523
- " sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]",
1524
1781
  " doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1525
1782
  " status [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1526
- " brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1527
- " apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1528
- " import --profile <id-or-path> --source-root <dir> [--profile-dirs a,b] [--out <path>]",
1529
- " import --profile <id-or-path> --source-root <dir> --config <path> --merge [--replace] [--dry-run]",
1530
- " scan --config <path>",
1783
+ " brief [--agent codex|claude|opencode|hermes] [--intent <request>] [--config <path>] [--include-actions] [--verbose] [--json]",
1784
+ " apply-action <action-id> [--agent codex|claude|opencode|hermes] [--intent <text>] [--config <path>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1785
+ " skill enable|disable <skill-id> [--dry-run] [--json]",
1786
+ " skill share|unshare <skill-id> [--dry-run] [--json]",
1787
+ " skill preference <skill-id> --intent <term>[,<term>] --priority <integer> [--dry-run] [--json]",
1788
+ " skill forget <skill-id> [--dry-run] [--json]",
1789
+ " migrate v2 [--config <path>] [--skills <dir>] [--yes] [--rollback <backup>] [--json]",
1531
1790
  " check --config <path> --skills <dir>",
1532
- " list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]",
1533
- " explain <skill-id> --config <path> --skills <dir> [--json]",
1534
- " route <intent> --workflow <name> --config <path> --skills <dir> [--json]",
1535
- " can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1536
- " guard use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1537
- " audit sources --config <path> --skills <dir> [--verify] [--json]",
1538
- " rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]",
1539
- " hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]",
1540
- " lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]",
1541
- " review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]",
1542
- " add skill <skill-id> --path <relative-skill-path> --config <path> --skills <dir> [--status <status>] [--invocation <mode>] [--exposure <exposure>] [--category <name>] [--workflow <name>] [--dry-run] [--json]",
1543
- " add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]",
1544
- " add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]",
1545
- " variant add <variant-id> --from <base-id> --capability <name> --workflow <name> --config <path> --skills <dir> [--path <relative-skill-path>] [--mode manual-only|router-only|workflow-auto] [--category <name>] [--owner-install-unit <unit-id>] [--dry-run] [--json]",
1546
- " variant fork <variant-id> --from <base-id> --capability <name> --workflow <name> --path <relative-skill-path> --config <path> --skills <dir> [--adapted-for <label>] [--category <name>] [--owner-install-unit <unit-id>] [--dry-run] [--json]",
1547
- " variant status <variant-id> --config <path> --skills <dir> [--json]",
1548
- " variant approve <variant-id> --config <path> --skills <dir> [--mode manual-only|router-only|workflow-auto] [--dry-run] [--json]",
1549
- " variant reset <variant-id> --to-base|--to-approved --config <path> --skills <dir> [--yes] [--dry-run] [--mode manual-only|router-only|workflow-auto] [--json]",
1550
- " activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]",
1551
- " block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]",
1552
- " quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]",
1553
- " prefer <skill-id> --workflow <name> --capability <name> --config <path> --skills <dir> [--dry-run] [--json]",
1554
- " remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]",
1555
- " dashboard --config <path> --skills <dir> [--out <path>]",
1556
- " reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]",
1557
- " impact disable <skill-id> --config <path> --skills <dir> [--out <path>] [--json]",
1791
+ " list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]",
1792
+ " explain <skill-id> [--config <path>] [--skills <dir>] [--json]",
1793
+ " route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1794
+ " can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1795
+ " guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1796
+ "",
1797
+ "Advanced operator commands:",
1798
+ " Import, audit, rollout, hook, lock, variant, reconcile, impact, dashboard, and v1 lifecycle commands remain available through command-specific help and docs/reference.md.",
1799
+ " Audit metadata never changes skill availability; runtime/action permission remains with the agent or harness.",
1800
+ "",
1801
+ "Legacy v1 project policy mode:",
1802
+ " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1803
+ " Deprecated project-local policy bootstrap; not needed for normal use.",
1558
1804
  "",
1559
- "AI/automation control loop:",
1560
- " Goal: keep skills broadly available while routing overlaps consistently; see docs/ai-skill-routing-goal.md.",
1561
- " Development loop: observe route work explain briefly ask after remember policy.",
1562
- " For an already-allowed skill, disclose the selected skill at start and completion; do not ask for another approval.",
1563
- " Translate an ambiguous request or explicit skill decision into the current brief: skillboard brief --json --config <path> --skills <dir> [--workflow <name>] [--intent <request>] [--include-actions].",
1564
- " If a policy-changing action is needed, pick one current action id from that brief and ask the user for one confirmation.",
1565
- " Apply one current action with skillboard apply-action <action-id> --config <path> --skills <dir> [--workflow <name>] --yes --json.",
1566
- " Read the returned post-apply brief, then run skillboard guard use automatically before invocation.",
1567
- " apply-action re-resolves current actions; do not use cached/stale ids, multiple actions, or raw action-card shell text as the primary apply path.",
1805
+ "v2 AI/automation control loop:",
1806
+ " Policy decides only enabled/disabled and per-skill opt-in sharing; see docs/ai-skill-routing-goal.md.",
1807
+ " Valid installed skills default to enabled and agent-local. Optional preference ranks only and never changes availability.",
1808
+ " Read brief --intent --agent <agent>, select an enabled installed skill, and run guard use automatically before use.",
1809
+ " For an allowed skill, work without another approval; ask once only before a persistent policy change.",
1810
+ " Source/provenance observations are audit metadata, never availability. Runtime/action authorization is outside SkillBoard.",
1811
+ " Stale v1 policy is read-only: preview with skillboard migrate v2 --config <path> --json, apply with --yes --json, or restore with --rollback <backup> --json.",
1568
1812
  ""
1569
1813
  ].join("\n");
1570
1814
  }
1571
1815
 
1572
1816
  function commandHelpText(command) {
1817
+ if (command === "skill") {
1818
+ return skillHelpText();
1819
+ }
1573
1820
  if (command === "brief") {
1574
1821
  return briefHelpText();
1575
1822
  }
@@ -1604,6 +1851,20 @@ function commandHelpText(command) {
1604
1851
  return usage === undefined ? null : genericCommandHelpText(usage);
1605
1852
  }
1606
1853
 
1854
+ function skillHelpText() {
1855
+ return [
1856
+ "Usage: skillboard skill enable|disable <skill-id> [--dry-run] [--json]",
1857
+ "Usage: skillboard skill share|unshare <skill-id> [--dry-run] [--json]",
1858
+ "Usage: skillboard skill preference <skill-id> --intent <term>[,<term>] --priority <integer> [--dry-run] [--json]",
1859
+ "Usage: skillboard skill forget <skill-id> [--dry-run] [--json]",
1860
+ "",
1861
+ "Policy records enablement and explicit per-skill sharing. Skills remain agent-local by default.",
1862
+ "Preference only ranks enabled skills installed for the current agent; it never changes availability.",
1863
+ "Forget removes policy only after an unshared skill is no longer present in generated inventory; it never deletes skill files.",
1864
+ ""
1865
+ ].join("\n");
1866
+ }
1867
+
1607
1868
  function genericCommandHelpText(usageLines) {
1608
1869
  return [
1609
1870
  ...usageLines.map((line) => `Usage: skillboard ${line}`),
@@ -1618,8 +1879,9 @@ function initHelpText() {
1618
1879
  return [
1619
1880
  "Usage: skillboard init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1620
1881
  "",
1621
- "Creates the local SkillBoard control files for a project.",
1622
- "Use it once per project before asking the AI which skills it can use.",
1882
+ "Deprecated project-local policy bootstrap. This command is not needed for normal use.",
1883
+ "Normal flow: install agent-skillboard globally, let postinstall/setup refresh agent-layer guidance, then ask your AI normal work requests.",
1884
+ "Use init only for an existing workspace that intentionally maintains local SkillBoard policy files.",
1623
1885
  "",
1624
1886
  "Options:",
1625
1887
  " --dir <path> Project root to initialize; defaults to the current directory.",
@@ -1628,10 +1890,9 @@ function initHelpText() {
1628
1890
  "",
1629
1891
  "What changes:",
1630
1892
  " Writes skillboard.config.yaml, skills/, .skillboard/, AGENTS.md, and CLAUDE.md as needed.",
1631
- " Imports trusted user-local skills as on-request skills.",
1632
- " Keeps runtime, plugin, system, and external skills behind source review before manual activation.",
1893
+ " Scans installed skills into generated inventory without source-based authorization.",
1633
1894
  "",
1634
- "Next:",
1895
+ "Legacy next steps:",
1635
1896
  " Run skillboard doctor --summary.",
1636
1897
  " Ask your AI: \"What skills can you use in this project?\"",
1637
1898
  ""
@@ -1645,14 +1906,14 @@ function setupHelpText() {
1645
1906
  "Installs or refreshes SkillBoard at the agent layer, not into a project.",
1646
1907
  "A normal global package install already runs this automatically for detected supported agents.",
1647
1908
  "Use setup later after adding another supported agent, enabling a new agent home, or skipping install scripts.",
1648
- "You do not need skillboard init for this install-time setup; init is only for a workspace where you want project-local policy files.",
1909
+ "skillboard init is deprecated project-local policy bootstrap and is not needed for normal use.",
1649
1910
  "Without --yes, setup explains the user agent skill files it will write and asks before installing when run in a TTY.",
1650
1911
  "In non-interactive automation, rerun with --yes after choosing the target agents.",
1651
1912
  "",
1652
1913
  "What changes after confirmation:",
1653
1914
  " Writes a SkillBoard guidance skill into detected user agent skill roots.",
1654
- " Does not write skillboard.config.yaml, .skillboard/, AGENTS.md, or CLAUDE.md in projects.",
1655
- " Teaches agents to use installed skills by default and resolve overlap by workflow priority.",
1915
+ " Creates ~/skillboard.config.yaml and ~/.skillboard/inventory.json; it writes no project files.",
1916
+ " Teaches agents to use local skills by default and share only user-selected skills.",
1656
1917
  " Remove this managed guidance later with skillboard uninstall --agent-layer.",
1657
1918
  "",
1658
1919
  "Supported agent homes:",
@@ -1697,14 +1958,17 @@ function importSkillHelpText() {
1697
1958
 
1698
1959
  function uninstallHelpText() {
1699
1960
  return [
1961
+ "Usage: skillboard uninstall --user (--dry-run|--yes) [--json]",
1700
1962
  "Usage: skillboard uninstall [--dir <path>] [--dry-run] [--keep-settings] [--purge] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs] [--agent-layer] [--agent codex[,claude,opencode,hermes]]",
1701
1963
  "",
1702
1964
  "This help is read-only. It does not load config or change project files.",
1703
1965
  "",
1704
- "Removes SkillBoard project bridge files, generated lifecycle scaffolding, or managed agent-layer guidance.",
1966
+ "Removes complete user-level SkillBoard state, project bridge files, generated lifecycle scaffolding, or managed agent-layer guidance.",
1705
1967
  "Default project cleanup removes SkillBoard settings and generated project state while preserving local skills and user-authored non-SkillBoard content.",
1706
1968
  "",
1707
1969
  "Options:",
1970
+ " --user Remove all marker-owned shared copies, managed guidance, home policy, and generated user state.",
1971
+ " --yes Confirm the user-level removal after previewing it.",
1708
1972
  " --dir <path> Project root to clean up; defaults to the current directory.",
1709
1973
  " --dry-run Preview the cleanup without writing changes.",
1710
1974
  " --keep-settings Preserve project SkillBoard settings and bridge guidance during default cleanup.",
@@ -1716,6 +1980,12 @@ function uninstallHelpText() {
1716
1980
  " --keep-empty-dirs Preserve empty generated directories.",
1717
1981
  " --agent-layer Remove managed user-agent skillboard guidance instead of project files.",
1718
1982
  " --agent <list> Target supported agents for --agent-layer cleanup.",
1983
+ " --json Print a machine-readable user-level removal plan or result.",
1984
+ "",
1985
+ "User-level cleanup:",
1986
+ " Run skillboard uninstall --user --dry-run, inspect the complete plan, then apply it with --yes.",
1987
+ " Removes only marker-owned shared copies and managed guidance; agent-owned and unmanaged skills are preserved.",
1988
+ " Removes ~/skillboard.config.yaml and ~/.skillboard only after managed copies and guidance are handled.",
1719
1989
  "",
1720
1990
  "Default project cleanup:",
1721
1991
  " Removes SkillBoard config, bridge blocks, and the entire .skillboard/ project state directory.",
@@ -1736,11 +2006,11 @@ function doctorHelpText() {
1736
2006
  return [
1737
2007
  "Usage: skillboard doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1738
2008
  "",
1739
- "Checks whether a SkillBoard project is ready to use.",
2009
+ "Checks the user-level policy and generated inventory health.",
1740
2010
  "The status command is an alias for doctor.",
1741
2011
  "",
1742
2012
  "Options:",
1743
- " --dir <path> Project root to check; defaults to the current directory.",
2013
+ " --dir <path> Advanced legacy workspace root override.",
1744
2014
  " --config <path> Use a specific skillboard.config.yaml.",
1745
2015
  " --skills <dir> Use a specific skills directory.",
1746
2016
  " --verify Verify local source/cache digests when available.",
@@ -1748,23 +2018,23 @@ function doctorHelpText() {
1748
2018
  " --summary Print a short human summary.",
1749
2019
  " --json Print an agent-readable payload.",
1750
2020
  "",
1751
- "Use this after init and before trusting new skill sources.",
2021
+ "Without path overrides, this checks ~/skillboard.config.yaml and ~/.skillboard/inventory.json from any directory.",
2022
+ "It is read-only.",
1752
2023
  ""
1753
2024
  ].join("\n");
1754
2025
  }
1755
2026
 
1756
2027
  function briefHelpText() {
1757
2028
  return [
1758
- "Usage: skillboard brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
2029
+ "Usage: skillboard brief [--agent codex|claude|opencode|hermes] [--intent <request>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1759
2030
  "",
1760
- "Reads the current SkillBoard brief without changing project files.",
2031
+ "Reads the current user-level SkillBoard brief without changing files.",
1761
2032
  "Use it when a user asks what skills the AI can use, a request has ambiguous skill overlap, or a policy change needs approval.",
1762
2033
  "",
1763
2034
  "Options:",
1764
- " --workflow <name> Evaluate one workflow.",
2035
+ " --agent <name> Evaluate actual installation for the current agent; required for v2 availability and routing.",
1765
2036
  " --intent <request> Add a natural-language request so SkillBoard can suggest a skill.",
1766
- " --dir <path> Use a project root; defaults to the current directory.",
1767
- " --config <path> Use a specific skillboard.config.yaml.",
2037
+ " --config <path> Advanced policy override; defaults to ~/skillboard.config.yaml.",
1768
2038
  " --skills <dir> Use a specific skills directory.",
1769
2039
  " --include-actions Include current action ids in JSON output.",
1770
2040
  " --verbose Show full lists instead of compact previews.",
@@ -1772,7 +2042,7 @@ function briefHelpText() {
1772
2042
  "",
1773
2043
  "AI use:",
1774
2044
  " Read this before answering availability questions.",
1775
- " Run skillboard guard use <skill-id> --workflow <name> before invoking a skill.",
2045
+ " Run skillboard guard use <skill-id> --agent <agent> before invoking a skill.",
1776
2046
  " If guard allows use, disclose the skill at the start and completion; do not ask for another approval.",
1777
2047
  " If a policy change is needed, ask the user to approve one current action id from this brief.",
1778
2048
  ""
@@ -1781,14 +2051,14 @@ function briefHelpText() {
1781
2051
 
1782
2052
  function routeHelpText() {
1783
2053
  return [
1784
- "Usage: skillboard route <intent> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2054
+ "Usage: skillboard route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1785
2055
  "",
1786
2056
  "Suggests the routed skill for a user request when several allowed skills may overlap.",
1787
2057
  "Use it when the AI needs a skill recommendation without changing policy.",
1788
2058
  "",
1789
2059
  "Options:",
1790
2060
  " <intent> Natural-language request, such as \"write tests first\".",
1791
- " --workflow <name> Workflow to route within.",
2061
+ " --agent <name> Route among skills installed for this agent.",
1792
2062
  " --config <path> Use a specific skillboard.config.yaml.",
1793
2063
  " --skills <dir> Use a specific skills directory.",
1794
2064
  " --json Print an agent-readable payload.",
@@ -1804,14 +2074,14 @@ function routeHelpText() {
1804
2074
 
1805
2075
  function canUseHelpText() {
1806
2076
  return [
1807
- "Usage: skillboard can-use <skill-id> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2077
+ "Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1808
2078
  "",
1809
- "Checks whether one skill is currently usable in a workflow without changing policy.",
2079
+ "Checks whether one skill is enabled and installed for the selected agent without changing policy.",
1810
2080
  "Use it when the AI needs an availability answer for a named skill.",
1811
2081
  "",
1812
2082
  "Options:",
1813
2083
  " <skill-id> Skill id to check.",
1814
- " --workflow <name> Workflow that would use the skill.",
2084
+ " --agent <name> Agent that would use the skill.",
1815
2085
  " --config <path> Use a specific skillboard.config.yaml.",
1816
2086
  " --skills <dir> Use a specific skills directory.",
1817
2087
  " --json Print an agent-readable payload.",
@@ -1826,7 +2096,7 @@ function canUseHelpText() {
1826
2096
 
1827
2097
  function guardHelpText() {
1828
2098
  return [
1829
- "Usage: skillboard guard use <skill-id> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2099
+ "Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1830
2100
  "",
1831
2101
  "Checks whether one skill may be used right now.",
1832
2102
  "Run this immediately before the AI invokes a skill.",
@@ -1834,7 +2104,7 @@ function guardHelpText() {
1834
2104
  "Options:",
1835
2105
  " use Guard a skill invocation.",
1836
2106
  " <skill-id> Skill id to check.",
1837
- " --workflow <name> Workflow that will use the skill.",
2107
+ " --agent <name> Agent that will use the skill.",
1838
2108
  " --config <path> Use a specific skillboard.config.yaml.",
1839
2109
  " --skills <dir> Use a specific skills directory.",
1840
2110
  " --json Print an agent-readable payload.",
@@ -1848,15 +2118,16 @@ function guardHelpText() {
1848
2118
 
1849
2119
  function applyActionHelpText() {
1850
2120
  return [
1851
- "Usage: skillboard apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
2121
+ "Usage: skillboard apply-action <action-id> [--agent codex|claude|opencode|hermes] [--intent <text>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1852
2122
  "",
1853
2123
  "Applies one current action id from the latest brief.",
1854
2124
  "Use it only after the user confirms a policy-changing action.",
1855
2125
  "",
1856
2126
  "Options:",
1857
2127
  " <action-id> One action id from the current brief.",
1858
- " --workflow <name> Workflow for workflow-scoped actions.",
1859
- " --dir <path> Project root; defaults to the current directory.",
2128
+ " --agent <name> Preserve the brief's current-agent context in preview and post-apply output.",
2129
+ " --intent <text> Intent bound to a current preference action.",
2130
+ " --dir <path> Advanced legacy workspace root override.",
1860
2131
  " --config <path> Use a specific skillboard.config.yaml.",
1861
2132
  " --skills <dir> Use a specific skills directory.",
1862
2133
  " --dry-run Preview the action without writing changes.",