agent-skillboard 0.2.18 → 0.3.1

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 (82) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +161 -255
  3. package/bin/postinstall.mjs +2 -1
  4. package/docs/adapters.md +37 -113
  5. package/docs/ai-skill-routing-goal.md +41 -108
  6. package/docs/capabilities.md +17 -104
  7. package/docs/install.md +70 -473
  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 +133 -362
  12. package/docs/rollout-runbook.md +23 -25
  13. package/docs/routing.md +23 -90
  14. package/docs/user-flow.md +68 -279
  15. package/docs/value-proof.md +23 -181
  16. package/docs/variant-lifecycle.md +47 -67
  17. package/docs/versioning.md +49 -269
  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 +96 -13
  29. package/src/agent-integration-content.mjs +21 -11
  30. package/src/agent-integration-files.mjs +1 -1
  31. package/src/agent-integration-home.mjs +14 -1
  32. package/src/agent-inventory-platforms.mjs +21 -8
  33. package/src/agent-inventory.mjs +44 -16
  34. package/src/agent-root-registry.mjs +127 -0
  35. package/src/agent-skill-import.mjs +2 -2
  36. package/src/agent-skill-roots.mjs +70 -13
  37. package/src/audit-paths.mjs +42 -0
  38. package/src/brief-cli.mjs +3 -2
  39. package/src/brief-renderer.mjs +1 -0
  40. package/src/cli.mjs +521 -235
  41. package/src/compatibility.mjs +24 -0
  42. package/src/control/can-use-guard.mjs +21 -1
  43. package/src/control/config-write.mjs +32 -2
  44. package/src/control/skill-crud.mjs +5 -0
  45. package/src/control/v2-guard.mjs +175 -0
  46. package/src/control/v2-skill-crud.mjs +32 -0
  47. package/src/control/v2-skill-forget.mjs +38 -0
  48. package/src/control/variant-status.mjs +47 -1
  49. package/src/control.mjs +55 -0
  50. package/src/doctor.mjs +71 -5
  51. package/src/domain/v2-policy.mjs +111 -0
  52. package/src/hook-plan.mjs +33 -3
  53. package/src/impact.mjs +52 -29
  54. package/src/index.mjs +25 -1
  55. package/src/init.mjs +50 -34
  56. package/src/install-health.mjs +177 -0
  57. package/src/inventory-install-units.mjs +63 -0
  58. package/src/inventory-json.mjs +279 -0
  59. package/src/inventory-refresh.mjs +168 -19
  60. package/src/lifecycle-cli.mjs +40 -12
  61. package/src/lifecycle-content.mjs +52 -67
  62. package/src/migration/v1-to-v2.mjs +212 -0
  63. package/src/migration/v2-files.mjs +211 -0
  64. package/src/migration/v2-journal.mjs +169 -0
  65. package/src/migration/v2-projection.mjs +108 -0
  66. package/src/migration/v2-transaction.mjs +205 -0
  67. package/src/policy.mjs +3 -0
  68. package/src/reconcile.mjs +139 -111
  69. package/src/report.mjs +168 -148
  70. package/src/review.mjs +2 -0
  71. package/src/route-advisory.mjs +47 -2
  72. package/src/route-selection.mjs +38 -2
  73. package/src/route.mjs +62 -2
  74. package/src/shared-skill-reconcile.mjs +97 -0
  75. package/src/shared-skill.mjs +325 -0
  76. package/src/source-digest.mjs +42 -0
  77. package/src/source-profiles.mjs +171 -144
  78. package/src/source-verification.mjs +32 -48
  79. package/src/uninstall.mjs +22 -0
  80. package/src/user-state-paths.mjs +19 -0
  81. package/src/user-uninstall.mjs +161 -0
  82. package/src/workspace.mjs +119 -79
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([
67
- ["setup", ["setup [--yes] [--agent codex[,claude,opencode,hermes]]"]],
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]"]],
79
+ ["setup", ["setup [--yes] [--agent codex[,claude,opencode,hermes]] [--skill-root <path>]"]],
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);
@@ -142,20 +155,25 @@ async function run(argv, stdout, stderr, stdin) {
142
155
  return await sources(argv.slice(1), options, stdout);
143
156
  case "import":
144
157
  return await importProfile(options, stdout);
145
- case "scan":
146
- return await scan(options, stdout);
158
+ case "scan":
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":
@@ -192,10 +210,10 @@ async function run(argv, stdout, stderr, stdin) {
192
210
  return await remove(argv.slice(1), options, stdout);
193
211
  case "dashboard":
194
212
  return await dashboard(options, stdout);
195
- case "reconcile":
196
- return await reconcile(options, stdout);
197
- case "impact":
198
- return await impact(argv.slice(1), options, stdout);
213
+ case "reconcile":
214
+ return await reconcile(options, stdout);
215
+ case "impact":
216
+ return await impact(argv.slice(1), options, stdout);
199
217
  case "help":
200
218
  case "--help":
201
219
  case "-h":
@@ -224,50 +242,88 @@ function selectedHelpText(command, args, options) {
224
242
  }
225
243
  return null;
226
244
  }
227
-
245
+
228
246
  async function importProfile(options, stdout) {
229
- const profileRef = options.get("profile");
230
- const sourceRoot = options.get("source-root");
231
- if (profileRef === undefined || sourceRoot === undefined) {
232
- throw new Error("Usage: skillboard import --profile <id-or-path> --source-root <dir>");
233
- }
234
- const profile = await loadSourceProfile(profileRef, { profileDirs: readCsv(options.get("profile-dirs")) });
235
- const imported = await importSource({ profile, sourceRoot });
236
- if (options.get("merge") === "true") {
237
- return await mergeImport(options, imported, stdout);
238
- }
239
- const fragment = renderImportFragment(imported);
240
- const out = options.get("out");
241
- if (out === undefined) {
242
- stdout.write(fragment);
243
- return 0;
244
- }
245
- await mkdir(dirname(out), { recursive: true });
246
- await writeFile(out, fragment, "utf8");
247
- stdout.write(`Import fragment written: ${out}\n`);
248
- return 0;
249
- }
250
-
247
+ assertKnownOptions("import", options, new Set(["profile", "source-root", "profile-dirs", "merge", "out", "config", "skills", "replace", "dry-run", "json"]));
248
+ const profileRef = options.get("profile");
249
+ const sourceRoot = options.get("source-root");
250
+ if (profileRef === undefined || sourceRoot === undefined) {
251
+ throw new Error("Usage: skillboard import --profile <id-or-path> --source-root <dir>");
252
+ }
253
+ const profile = await loadSourceProfile(profileRef, { profileDirs: readCsv(options.get("profile-dirs")) });
254
+ const imported = await importSource({ profile, sourceRoot });
255
+ if (options.get("merge") === "true") {
256
+ return await mergeImport(options, imported, stdout);
257
+ }
258
+ const fragment = renderImportFragment(imported);
259
+ const out = options.get("out");
260
+ if (out === undefined) {
261
+ stdout.write(fragment);
262
+ return 0;
263
+ }
264
+ await mkdir(dirname(out), { recursive: true });
265
+ await writeFile(out, fragment, "utf8");
266
+ stdout.write(`Import fragment written: ${out}\n`);
267
+ return 0;
268
+ }
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;
@@ -337,30 +401,60 @@ async function sources(argv, options, stdout) {
337
401
  writeOutput(stdout, result, options, () => renderSourceRefresh(result));
338
402
  return 0;
339
403
  }
340
-
404
+
341
405
  async function scan(options, stdout) {
342
- const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
343
- stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
344
- return 0;
345
- }
346
-
406
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
407
+ stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
408
+ return 0;
409
+ }
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
- const output = [...result.errors, ...result.warnings].join("\n");
351
- if (output.length > 0) {
352
- (result.ok ? stdout : stderr).write(`${output}\n`);
353
- }
354
- stdout.write(result.ok ? "Policy check passed\n" : "Policy check failed\n");
415
+ const output = [...result.errors, ...result.warnings].join("\n");
416
+ if (output.length > 0) {
417
+ (result.ok ? stdout : stderr).write(`${output}\n`);
418
+ }
419
+ stdout.write(result.ok ? "Policy check passed\n" : "Policy check failed\n");
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
- verifySources: options.get("verify") === "true"
454
+ verifySources: options.get("verify") === "true",
455
+ entrypointPath: process.argv[1],
456
+ env: process.env,
457
+ packageVersion: VERSION
364
458
  });
365
459
  const summary = options.get("summary") === "true";
366
460
  writeOutput(stdout, result, options, () => summary ? renderDoctorSummary(result) : renderDoctor(result));
@@ -369,6 +463,9 @@ async function doctor(options, stdout) {
369
463
 
370
464
  async function applyActionCommand(argv, options, stdout, stderr) {
371
465
  try {
466
+ assertKnownOptions("apply-action", options, new Set([
467
+ "agent", "workflow", "intent", "dir", "config", "skills", "dry-run", "yes", "allow-destructive", "out", "skillboard-bin", "json"
468
+ ]));
372
469
  const actionIds = positionalArgs(argv, APPLY_ACTION_VALUE_OPTIONS);
373
470
  if (actionIds.length !== 1) {
374
471
  throw new ApplyActionError(
@@ -377,10 +474,16 @@ async function applyActionCommand(argv, options, stdout, stderr) {
377
474
  );
378
475
  }
379
476
  const [actionId] = actionIds;
477
+ const state = statePaths(options);
380
478
  const result = await applyAdvisorAction(actionId, {
381
479
  root: commandRoot(options),
480
+ agent: options.get("agent"),
382
481
  workflow: options.get("workflow"),
482
+ intent: options.get("intent"),
383
483
  configPath: configPath(options),
484
+ inventoryPath: state.inventoryPath,
485
+ home: state.home,
486
+ env: process.env,
384
487
  skillsRoot: skillsRoot(options),
385
488
  dryRun: options.get("dry-run") === "true",
386
489
  yes: options.get("yes") === "true",
@@ -401,7 +504,47 @@ async function applyActionCommand(argv, options, stdout, stderr) {
401
504
  }
402
505
  }
403
506
 
507
+ async function skillPolicy(argv, options, stdout) {
508
+ assertKnownOptions("skill", options, new Set(["config", "skills", "dry-run", "json", "intent", "priority"]));
509
+ const args = positionalArgs(argv);
510
+ const operation = args[0];
511
+ const skillId = args[1];
512
+ if (skillId === undefined) throw new Error("Usage: skillboard skill enable|disable|share|unshare|preference|forget <skill-id>");
513
+ const common = { skillId, configPath: configPath(options), skillsRoot: skillsRoot(options), dryRun: options.get("dry-run") === "true" };
514
+ let result;
515
+ if (operation === "enable" || operation === "disable") {
516
+ result = await setV2SkillEnabled({ ...common, enabled: operation === "enable" });
517
+ } else if (operation === "share" || operation === "unshare") {
518
+ const state = statePaths(options);
519
+ result = await setSkillSharing({
520
+ ...common,
521
+ shared: operation === "share",
522
+ configPath: state.configPath,
523
+ inventoryPath: state.inventoryPath,
524
+ home: state.home,
525
+ env: process.env
526
+ });
527
+ writeOutput(stdout, result, options, () => `${operation === "share" ? "Shared" : "Unshared"} ${skillId}\n`);
528
+ return 0;
529
+ } else if (operation === "preference") {
530
+ const priority = Number(options.get("priority"));
531
+ result = await setV2SkillPreference({ ...common, intents: readCsv(options.get("intent")), priority });
532
+ } else if (operation === "forget") {
533
+ const state = statePaths(options);
534
+ result = await forgetV2Skill({
535
+ ...common,
536
+ configPath: state.configPath,
537
+ inventoryPath: state.inventoryPath
538
+ });
539
+ } else {
540
+ throw new Error(`Unknown skill policy operation: ${operation}`);
541
+ }
542
+ writeControlResult(stdout, result, options);
543
+ return 0;
544
+ }
545
+
404
546
  async function list(argv, options, stdout) {
547
+ assertKnownOptions("list", options, new Set(["workflow", "config", "skills", "json"]));
405
548
  const kind = positionalArgs(argv)[0] ?? "skills";
406
549
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
407
550
  let result;
@@ -421,6 +564,7 @@ async function list(argv, options, stdout) {
421
564
  }
422
565
 
423
566
  async function explain(argv, options, stdout) {
567
+ assertKnownOptions("explain", options, new Set(["config", "skills", "json"]));
424
568
  const skillId = positionalArgs(argv)[0];
425
569
  if (skillId === undefined) {
426
570
  throw new Error("Usage: skillboard explain <skill-id>");
@@ -432,17 +576,23 @@ async function explain(argv, options, stdout) {
432
576
  }
433
577
 
434
578
  async function route(argv, options, stdout) {
579
+ assertKnownOptions("route", options, new Set(["agent", "workflow", "config", "skills", "json"]));
435
580
  const intent = positionalArgs(argv).join(" ").trim();
436
581
  const workflow = options.get("workflow");
437
- if (intent.length === 0 || workflow === undefined) {
438
- throw new Error("Usage: skillboard route <intent> --workflow <name>");
582
+ if (intent.length === 0) {
583
+ throw new Error("Usage: skillboard route <intent> [--agent <name>]");
439
584
  }
440
585
  const config = configPath(options);
441
586
  const skills = skillsRoot(options);
442
587
  const workspace = await loadWorkspace({ configPath: config, skillsRoot: skills });
588
+ assertV2AgentOption(workspace, options, "skillboard route <intent> --agent <name>");
589
+ if (workspace.version === 1 && workflow === undefined) {
590
+ throw new Error("Usage: skillboard route <intent> --workflow <name>");
591
+ }
443
592
  const result = routeSkill(workspace, {
444
593
  intent,
445
594
  workflow,
595
+ agent: options.get("agent"),
446
596
  configPath: config,
447
597
  skillsRoot: skills
448
598
  });
@@ -451,26 +601,44 @@ async function route(argv, options, stdout) {
451
601
  }
452
602
 
453
603
  async function canUse(argv, options, stdout) {
604
+ assertKnownOptions("can-use", options, new Set(["agent", "workflow", "config", "skills", "json"]));
454
605
  const skillId = positionalArgs(argv)[0];
455
606
  const workflow = options.get("workflow");
456
- if (skillId === undefined || workflow === undefined) {
457
- throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
607
+ if (skillId === undefined) {
608
+ throw new Error("Usage: skillboard can-use <skill-id> [--agent <name>]");
458
609
  }
459
610
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
460
- const result = canUseSkill(workspace, skillId, workflow);
611
+ assertV2AgentOption(workspace, options, "skillboard can-use <skill-id> --agent <name>");
612
+ if (workspace.version === 1 && workflow === undefined) {
613
+ throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
614
+ }
615
+ const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
461
616
  writeOutput(stdout, result, options, () => renderCanUse(result));
462
617
  return result.allowed ? 0 : 2;
463
618
  }
464
619
 
465
620
  async function guard(argv, options, stdout) {
621
+ assertKnownOptions("guard", options, new Set(["agent", "workflow", "config", "skills", "json", "hook-projection-version"]));
466
622
  const args = positionalArgs(argv);
467
623
  const skillId = args[0] === "use" ? args[1] : args[0];
468
624
  const workflow = options.get("workflow");
469
- if (skillId === undefined || workflow === undefined) {
470
- throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
625
+ if (skillId === undefined) {
626
+ throw new Error("Usage: skillboard guard use <skill-id> [--agent <name>]");
471
627
  }
472
628
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
473
- const result = canUseSkill(workspace, skillId, workflow);
629
+ const hookProjection = options.get("hook-projection-version") ?? process.env.SKILLBOARD_POLICY_PROJECTION_VERSION;
630
+ if (args[0] !== "use" && hookProjection === undefined) {
631
+ throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
632
+ }
633
+ if (process.env.SKILLBOARD_SKILL_ID !== undefined && hookProjection === undefined) {
634
+ throw new Error("This pre-v2 policy projection is stale; regenerate the guard hook from version 2 policy.");
635
+ }
636
+ assertCurrentProjectionVersion(hookProjection, workspace.version);
637
+ assertV2AgentOption(workspace, options, "skillboard guard use <skill-id> --agent <name>");
638
+ if (workspace.version === 1 && workflow === undefined) {
639
+ throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
640
+ }
641
+ const result = canUseSkill(workspace, skillId, workflow, options.get("agent"));
474
642
  if (options.get("json") === "true") {
475
643
  stdout.write(`${JSON.stringify(result, null, 2)}\n`);
476
644
  } else {
@@ -480,6 +648,7 @@ async function guard(argv, options, stdout) {
480
648
  }
481
649
 
482
650
  async function audit(argv, options, stdout) {
651
+ assertKnownOptions("audit", options, new Set(["verify", "config", "skills", "json"]));
483
652
  const args = positionalArgs(argv);
484
653
  if ((args[0] ?? "sources") !== "sources") {
485
654
  throw new Error("Usage: skillboard audit sources --config <path> --skills <dir> [--json]");
@@ -520,6 +689,7 @@ async function rollout(argv, options, stdout) {
520
689
  }
521
690
 
522
691
  async function hook(argv, options, stdout) {
692
+ assertKnownOptions("hook", options, new Set(["workflow", "out", "skillboard-bin", "config", "skills", "dry-run", "json"]));
523
693
  const args = positionalArgs(argv);
524
694
  if (args[0] !== "install") {
525
695
  throw new Error("Usage: skillboard hook install --workflow <name> [--out <path>] [--skillboard-bin <path>] [--dry-run]");
@@ -704,6 +874,10 @@ async function addHarnessCommand(args, options, stdout) {
704
874
 
705
875
  async function variant(argv, options, stdout) {
706
876
  try {
877
+ assertKnownOptions("variant", options, new Set([
878
+ "from", "capability", "workflow", "path", "mode", "category", "owner-install-unit",
879
+ "adapted-for", "config", "skills", "to-base", "to-approved", "yes", "dry-run", "json"
880
+ ]));
707
881
  return await runVariant(argv, options, stdout);
708
882
  } catch (error) {
709
883
  const payload = variantErrorPayload(error);
@@ -718,6 +892,9 @@ async function variant(argv, options, stdout) {
718
892
  async function runVariant(argv, options, stdout) {
719
893
  const args = positionalArgs(argv);
720
894
  const subcommand = args[0];
895
+ if (options.get("config") !== undefined && ["add", "fork", "status", "approve", "reset"].includes(subcommand)) {
896
+ await assertVariantPolicyBoundary(configPath(options), subcommand);
897
+ }
721
898
  if (subcommand === "add") {
722
899
  return await variantAddCommand(args, options, stdout);
723
900
  }
@@ -736,6 +913,20 @@ async function runVariant(argv, options, stdout) {
736
913
  throw new VariantCliError("unknown_variant_subcommand", `Unknown variant subcommand: ${subcommand ?? "<missing>"}`, 2);
737
914
  }
738
915
 
916
+ async function assertVariantPolicyBoundary(path, subcommand) {
917
+ const document = YAML.parseDocument(await readFile(path, "utf8"));
918
+ if (document.errors.length > 0) {
919
+ throw new Error(`Invalid YAML config: ${document.errors.map((error) => error.message).join("; ")}`);
920
+ }
921
+ if (document.get("version") === 2 && subcommand !== "status") {
922
+ throw new VariantCliError(
923
+ "compatibility_error",
924
+ "Variant policy mutation is unavailable for version 2; use skill enable, disable, share, unshare, or preference. Read-only variant status remains available.",
925
+ 1
926
+ );
927
+ }
928
+ }
929
+
739
930
  async function variantAddCommand(args, options, stdout) {
740
931
  const variantId = args[1];
741
932
  const baseId = options.get("from");
@@ -983,37 +1174,40 @@ async function remove(argv, options, stdout) {
983
1174
  }
984
1175
 
985
1176
  async function dashboard(options, stdout) {
986
- const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
987
- const markdown = renderDashboard(workspace);
988
- const out = options.get("out");
989
- if (out === undefined) {
990
- stdout.write(markdown);
991
- return 0;
992
- }
993
- await mkdir(dirname(out), { recursive: true });
994
- await writeFile(out, markdown, "utf8");
995
- stdout.write(`Dashboard written: ${out}\n`);
996
- return 0;
997
- }
998
-
999
- async function reconcile(options, stdout) {
1000
- const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1001
- const plan = reconcileWorkspace(workspace, { actualHarnesses: readCsv(options.get("actual-harnesses")) });
1002
- const markdown = renderReconcilePlan(plan);
1003
- const out = options.get("out");
1004
- if (out === undefined) {
1005
- stdout.write(markdown);
1006
- return 0;
1007
- }
1008
- await mkdir(dirname(out), { recursive: true });
1009
- await writeFile(out, markdown, "utf8");
1010
- stdout.write(`Reconcile plan written: ${out}\n`);
1011
- return 0;
1012
- }
1013
-
1014
- async function impact(argv, options, stdout) {
1015
- if (argv[0] !== "disable" || argv[1] === undefined) {
1016
- throw new Error("Usage: skillboard impact disable <skill-id>");
1177
+ assertKnownOptions("dashboard", options, new Set(["config", "skills", "out", "json"]));
1178
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1179
+ const markdown = renderDashboard(workspace);
1180
+ const out = options.get("out");
1181
+ if (out === undefined) {
1182
+ stdout.write(markdown);
1183
+ return 0;
1184
+ }
1185
+ await mkdir(dirname(out), { recursive: true });
1186
+ await writeFile(out, markdown, "utf8");
1187
+ stdout.write(`Dashboard written: ${out}\n`);
1188
+ return 0;
1189
+ }
1190
+
1191
+ async function reconcile(options, stdout) {
1192
+ assertKnownOptions("reconcile", options, new Set(["config", "skills", "actual-harnesses", "out", "json"]));
1193
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1194
+ const plan = reconcileWorkspace(workspace, { actualHarnesses: readCsv(options.get("actual-harnesses")) });
1195
+ const markdown = renderReconcilePlan(plan);
1196
+ const out = options.get("out");
1197
+ if (out === undefined) {
1198
+ stdout.write(markdown);
1199
+ return 0;
1200
+ }
1201
+ await mkdir(dirname(out), { recursive: true });
1202
+ await writeFile(out, markdown, "utf8");
1203
+ stdout.write(`Reconcile plan written: ${out}\n`);
1204
+ return 0;
1205
+ }
1206
+
1207
+ async function impact(argv, options, stdout) {
1208
+ assertKnownOptions("impact", options, new Set(["config", "skills", "out", "json"]));
1209
+ if (argv[0] !== "disable" || argv[1] === undefined) {
1210
+ throw new Error("Usage: skillboard impact disable <skill-id>");
1017
1211
  }
1018
1212
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1019
1213
  const result = impactDisable(workspace, argv[1]);
@@ -1023,22 +1217,22 @@ async function impact(argv, options, stdout) {
1023
1217
  }
1024
1218
  const markdown = renderImpact(result);
1025
1219
  const out = options.get("out");
1026
- if (out === undefined) {
1027
- stdout.write(markdown);
1028
- return 0;
1029
- }
1030
- await mkdir(dirname(out), { recursive: true });
1031
- await writeFile(out, markdown, "utf8");
1032
- stdout.write(`Impact report written: ${out}\n`);
1033
- return 0;
1034
- }
1035
-
1220
+ if (out === undefined) {
1221
+ stdout.write(markdown);
1222
+ return 0;
1223
+ }
1224
+ await mkdir(dirname(out), { recursive: true });
1225
+ await writeFile(out, markdown, "utf8");
1226
+ stdout.write(`Impact report written: ${out}\n`);
1227
+ return 0;
1228
+ }
1229
+
1036
1230
  function renderImpact(result) {
1037
- return [
1038
- `# Disable Impact: ${result.skillId}`,
1039
- "",
1040
- `- Found: \`${result.exists}\``,
1041
- `- Risk: ${result.risk}`,
1231
+ return [
1232
+ `# Disable Impact: ${result.skillId}`,
1233
+ "",
1234
+ `- Found: \`${result.exists}\``,
1235
+ `- Risk: ${result.risk}`,
1042
1236
  `- Affected workflows: ${formatList(result.affectedWorkflows)}`,
1043
1237
  `- Affected required outputs: ${formatList(result.affectedOutputs)}`,
1044
1238
  `- Alternatives: ${formatList(result.alternatives)}`,
@@ -1058,41 +1252,74 @@ function formatActiveConflicts(entries) {
1058
1252
  }
1059
1253
 
1060
1254
  function configPath(options) {
1061
- return options.get("config") ?? "skillboard.config.yaml";
1255
+ return statePaths(options).configPath;
1062
1256
  }
1063
1257
 
1064
1258
  function skillsRoot(options) {
1065
- return options.get("skills") ?? "skills";
1259
+ return options.get("skills");
1066
1260
  }
1067
1261
 
1068
1262
  function commandRoot(options) {
1263
+ return options.get("dir") ?? dirname(configPath(options));
1264
+ }
1265
+
1266
+ function statePaths(options) {
1069
1267
  const dir = options.get("dir");
1070
- if (dir !== undefined) {
1071
- return dir;
1072
- }
1073
- const config = options.get("config");
1074
- return config !== undefined && isAbsolute(config) ? dirname(config) : ".";
1075
- }
1076
-
1077
- function parseOptions(args) {
1078
- const options = new Map();
1079
- for (let index = 0; index < args.length; index += 1) {
1080
- const arg = args[index];
1081
- if (!arg.startsWith("--")) {
1082
- continue;
1083
- }
1084
- const key = arg.slice(2);
1085
- const value = args[index + 1];
1086
- if (value === undefined || value.startsWith("--")) {
1087
- options.set(key, "true");
1088
- continue;
1089
- }
1090
- options.set(key, value);
1091
- index += 1;
1092
- }
1093
- return options;
1094
- }
1095
-
1268
+ return resolveUserStatePaths({
1269
+ env: process.env,
1270
+ cwd: process.cwd(),
1271
+ configPath: options.get("config") ?? (dir === undefined ? undefined : resolve(dir, "skillboard.config.yaml"))
1272
+ });
1273
+ }
1274
+
1275
+ function assertV2AgentOption(workspace, options, usage) {
1276
+ if (workspace.version !== 2) return;
1277
+ const agent = options.get("agent");
1278
+ if (agent === undefined) {
1279
+ throw new Error(`Version 2 availability requires --agent. Usage: ${usage}`);
1280
+ }
1281
+ if (!supportedAgentNames().includes(agent)) {
1282
+ throw new Error(`Unsupported agent: ${agent}. Expected one of: ${supportedAgentNames().join(", ")}.`);
1283
+ }
1284
+ }
1285
+
1286
+ const VALUE_OPTIONS = new Set([
1287
+ "actual-harnesses", "adapted-file", "adapted-for", "agent", "cache-dir", "capability", "category",
1288
+ "command", "config", "config-file", "dir", "exposure", "from", "harness", "harness-status",
1289
+ "hook-projection-version", "install-output", "intent", "invocation", "kind", "mode", "out",
1290
+ "owner-install-unit", "path", "priority", "profile", "profile-dirs", "rollback", "rollouts-dir",
1291
+ "scan-root", "scope", "skill", "skill-root", "skillboard-bin", "skills", "source", "source-root", "status",
1292
+ "target-skill", "to", "transaction", "trust-level", "unit", "workflow"
1293
+ ]);
1294
+
1295
+ function parseOptions(args, command) {
1296
+ const options = new Map();
1297
+ for (let index = 0; index < args.length; index += 1) {
1298
+ const arg = args[index];
1299
+ if (!arg.startsWith("--")) {
1300
+ continue;
1301
+ }
1302
+ const key = arg.slice(2);
1303
+ const value = args[index + 1];
1304
+ if (value === undefined || value.startsWith("--")) {
1305
+ if (VALUE_OPTIONS.has(key)) throw new Error(`Missing value for ${command} option: --${key}`);
1306
+ options.set(key, "true");
1307
+ continue;
1308
+ }
1309
+ options.set(key, value);
1310
+ index += 1;
1311
+ }
1312
+ return options;
1313
+ }
1314
+
1315
+ function assertKnownOptions(command, options, allowed) {
1316
+ for (const option of options.keys()) {
1317
+ if (!allowed.has(option)) {
1318
+ throw new Error(`Unknown ${command} option: --${option}`);
1319
+ }
1320
+ }
1321
+ }
1322
+
1096
1323
  function formatList(values) {
1097
1324
  return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
1098
1325
  }
@@ -1105,7 +1332,7 @@ function readCsv(value) {
1105
1332
  if (value === undefined || value.trim() === "") {
1106
1333
  return [];
1107
1334
  }
1108
- return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1335
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1109
1336
  }
1110
1337
 
1111
1338
  function renderImportMerge(result) {
@@ -1124,8 +1351,9 @@ function renderImportMerge(result) {
1124
1351
  }
1125
1352
 
1126
1353
  function renderInventoryRefresh(result) {
1127
- return [
1354
+ const lines = [
1128
1355
  `${result.dryRun ? "Dry run: " : ""}Inventory refreshed: ${result.configPath}`,
1356
+ ...(result.bootstrappedV2 ? ["Bootstrapped minimal SkillBoard v2 policy."] : []),
1129
1357
  renderChangePlan(result.plan).trimEnd(),
1130
1358
  `Scanned skills: ${result.scan.scannedSkills}`,
1131
1359
  `Scanned install units: ${result.scan.scannedInstallUnits}`,
@@ -1138,7 +1366,8 @@ function renderInventoryRefresh(result) {
1138
1366
  `Review notes: ${formatList(result.scan.reviewNotes ?? [])}`,
1139
1367
  `Scan warnings: ${formatList(result.scan.warnings ?? [])}`,
1140
1368
  ""
1141
- ].join("\n");
1369
+ ];
1370
+ return lines.join("\n");
1142
1371
  }
1143
1372
 
1144
1373
  function renderInstallDetection(result) {
@@ -1303,6 +1532,10 @@ function renderList(kind, values) {
1303
1532
  }
1304
1533
  if (kind === "skills") {
1305
1534
  return `${values.map((skill) => {
1535
+ if (typeof skill.enabled === "boolean") {
1536
+ const inventory = skill.inventory ?? { path: skill.path ?? null, owner_install_unit: null };
1537
+ 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"}`;
1538
+ }
1306
1539
  const roles = skill.workflowRoles.length === 0 ? "none" : skill.workflowRoles.join(",");
1307
1540
  const owner = skill.ownerInstallUnit ?? "direct";
1308
1541
  return `${skill.id}\t${skill.status}\t${skill.invocation}\t${skill.sourceClass}\towner=${owner}\troles=${roles}`;
@@ -1324,6 +1557,18 @@ function renderList(kind, values) {
1324
1557
  }
1325
1558
 
1326
1559
  function renderExplain(result) {
1560
+ if (typeof result.enabled === "boolean") {
1561
+ const inventory = result.inventory ?? { path: result.path ?? null, owner_install_unit: null };
1562
+ return [
1563
+ `Skill: ${result.id}`,
1564
+ `Enabled: ${result.enabled}`,
1565
+ `Shared: ${result.shared}`,
1566
+ `Installed on: ${(inventory.installed_on ?? []).join(", ") || "none"}`,
1567
+ `Inventory path: ${inventory.path ?? "none"}`,
1568
+ `Inventory owner: ${inventory.owner_install_unit ?? "none"}`,
1569
+ ""
1570
+ ].join("\n");
1571
+ }
1327
1572
  const workflows = result.workflows.length === 0
1328
1573
  ? "none"
1329
1574
  : result.workflows.map((workflow) => `${workflow.workflow}:${workflow.roles.join(",")}`).join(", ");
@@ -1379,6 +1624,24 @@ function renderRoute(result) {
1379
1624
  return lines.join("\n");
1380
1625
  }
1381
1626
 
1627
+ function renderMigration(result) {
1628
+ const lines = [
1629
+ `SkillBoard v2 migration: ${result.mode}`,
1630
+ `Changed: ${result.changed}`,
1631
+ `Target version: ${result.target_version}`,
1632
+ `Input SHA-256: ${result.input_sha256}`,
1633
+ `Config SHA-256: ${result.config_sha256}`
1634
+ ];
1635
+ if (result.inventory_sha256 !== null) lines.push(`Inventory SHA-256: ${result.inventory_sha256}`);
1636
+ if (result.backup !== null) lines.push(`Backup: ${result.backup}`);
1637
+ if (result.counts !== undefined) {
1638
+ lines.push(`Skills: ${result.counts.skills} (${result.counts.enabled} enabled, ${result.counts.disabled} disabled)`);
1639
+ lines.push(`Warnings: ${result.counts.warnings}; losses: ${result.counts.losses}`);
1640
+ }
1641
+ lines.push("");
1642
+ return lines.join("\n");
1643
+ }
1644
+
1382
1645
  function renderSourceAudit(result) {
1383
1646
  const lines = [
1384
1647
  `Source audit: ${result.ok ? "passed" : "failed"}`,
@@ -1424,6 +1687,7 @@ function renderDoctorSummary(result) {
1424
1687
  const status = result.ok ? result.reviewRequired ? "safe mode, review needed" : "passed" : "needs attention";
1425
1688
  const lines = [
1426
1689
  `SkillBoard doctor: ${status}`,
1690
+ renderInstallation(result.installation),
1427
1691
  `Workspace: ${result.workspace.skills.declared} skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
1428
1692
  `Source audit: ${result.sources.ok ? "passed" : "failed"} (${result.sources.errors.length} errors, ${result.sources.warnings.length} warnings, ${result.sources.blockingWarnings.length} blocking)`,
1429
1693
  `Policy: ${result.policy.ok ? "passed" : "failed"} (${result.policy.errors.length} errors, ${result.policy.warnings.length} warnings)`
@@ -1457,6 +1721,7 @@ function renderDoctor(result) {
1457
1721
  const status = result.ok ? result.reviewRequired ? "safe mode, review needed" : "passed" : "needs attention";
1458
1722
  const lines = [
1459
1723
  `SkillBoard doctor: ${status}`,
1724
+ renderInstallation(result.installation),
1460
1725
  `Root: ${result.root}`,
1461
1726
  `Config: ${result.config.exists ? result.config.valid ? `valid v${result.config.version}` : `invalid (${result.config.error})` : "missing"}`,
1462
1727
  `Bridge: ${bridges}`,
@@ -1483,6 +1748,14 @@ function renderDoctor(result) {
1483
1748
  return lines.join("\n");
1484
1749
  }
1485
1750
 
1751
+ function renderInstallation(installation) {
1752
+ const currentVersion = installation.current.version ?? "unknown";
1753
+ const selected = installation.pathSelected === null
1754
+ ? "PATH selection not found"
1755
+ : `PATH selects ${installation.pathSelected.version ?? "unknown"} at ${installation.pathSelected.path}`;
1756
+ return `Installation: current ${currentVersion} at ${installation.current.entrypoint}; ${selected}; ${installation.installations.length} package installation(s) on PATH`;
1757
+ }
1758
+
1486
1759
  function appendNotInitializedAttachGuidance(lines, result) {
1487
1760
  if (result.mode !== "not-initialized") {
1488
1761
  return;
@@ -1511,68 +1784,53 @@ function helpText() {
1511
1784
  " The package postinstall auto-runs agent-layer guidance setup on install and update.",
1512
1785
  " Under sudo, setup targets SUDO_USER's agent homes while npm still controls the binary prefix.",
1513
1786
  " 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.",
1787
+ " Run skillboard doctor --summary after updates to detect PATH shadowing or duplicate global installs.",
1788
+ " Preview skillboard uninstall --user --dry-run before package removal to clean every managed artifact.",
1515
1789
  "",
1516
- "AI/automation operations:",
1790
+ "Core AI/automation operations:",
1517
1791
  " setup [--yes] [--agent codex[,claude,opencode,hermes]]",
1518
1792
  " import-skill --from <agent> --to <agent> --skill <id-or-dir> [--target-skill <id-or-dir>] [--adapted-file <path>] [--dry-run] [--yes] [--replace] [--json]",
1519
- " 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]]",
1793
+ " uninstall --user (--dry-run|--yes) [--json]",
1520
1794
  " inventory refresh [--dir <path>] [--config <path>] [--scan-root <dir>[,<dir>]] [--dry-run] [--json]",
1521
- " inventory detect --unit <id> --config <path> [--install-output <path>] [--config-file a,b] [--source <value>] [--kind <kind>] [--scope <scope>] [--dry-run] [--json]",
1522
- " sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]",
1523
1795
  " doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1524
1796
  " status [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1525
- " brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1526
- " apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1527
- " import --profile <id-or-path> --source-root <dir> [--profile-dirs a,b] [--out <path>]",
1528
- " import --profile <id-or-path> --source-root <dir> --config <path> --merge [--replace] [--dry-run]",
1529
- " scan --config <path>",
1797
+ " brief [--agent codex|claude|opencode|hermes] [--intent <request>] [--config <path>] [--include-actions] [--verbose] [--json]",
1798
+ " apply-action <action-id> [--agent codex|claude|opencode|hermes] [--intent <text>] [--config <path>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1799
+ " skill enable|disable <skill-id> [--dry-run] [--json]",
1800
+ " skill share|unshare <skill-id> [--dry-run] [--json]",
1801
+ " skill preference <skill-id> --intent <term>[,<term>] --priority <integer> [--dry-run] [--json]",
1802
+ " skill forget <skill-id> [--dry-run] [--json]",
1803
+ " migrate v2 [--config <path>] [--skills <dir>] [--yes] [--rollback <backup>] [--json]",
1530
1804
  " check --config <path> --skills <dir>",
1531
- " list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]",
1532
- " explain <skill-id> --config <path> --skills <dir> [--json]",
1533
- " route <intent> --workflow <name> --config <path> --skills <dir> [--json]",
1534
- " can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1535
- " guard use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1536
- " audit sources --config <path> --skills <dir> [--verify] [--json]",
1537
- " rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]",
1538
- " hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]",
1539
- " lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]",
1540
- " review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]",
1541
- " 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]",
1542
- " add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]",
1543
- " add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]",
1544
- " 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]",
1545
- " 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]",
1546
- " variant status <variant-id> --config <path> --skills <dir> [--json]",
1547
- " variant approve <variant-id> --config <path> --skills <dir> [--mode manual-only|router-only|workflow-auto] [--dry-run] [--json]",
1548
- " variant reset <variant-id> --to-base|--to-approved --config <path> --skills <dir> [--yes] [--dry-run] [--mode manual-only|router-only|workflow-auto] [--json]",
1549
- " activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]",
1550
- " block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]",
1551
- " quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]",
1552
- " prefer <skill-id> --workflow <name> --capability <name> --config <path> --skills <dir> [--dry-run] [--json]",
1553
- " remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]",
1554
- " dashboard --config <path> --skills <dir> [--out <path>]",
1555
- " reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]",
1556
- " impact disable <skill-id> --config <path> --skills <dir> [--out <path>] [--json]",
1805
+ " list [skills|workflows|harnesses|install-units] [--config <path>] [--skills <dir>] [--json]",
1806
+ " explain <skill-id> [--config <path>] [--skills <dir>] [--json]",
1807
+ " route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1808
+ " can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1809
+ " guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1557
1810
  "",
1558
- "Legacy project policy mode:",
1811
+ "Advanced operator commands:",
1812
+ " Import, audit, rollout, hook, lock, variant, reconcile, impact, dashboard, and v1 lifecycle commands remain available through command-specific help and docs/reference.md.",
1813
+ " Audit metadata never changes skill availability; runtime/action permission remains with the agent or harness.",
1814
+ "",
1815
+ "Legacy v1 project policy mode:",
1559
1816
  " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1560
1817
  " Deprecated project-local policy bootstrap; not needed for normal use.",
1561
1818
  "",
1562
- "AI/automation control loop:",
1563
- " Goal: keep skills broadly available while routing overlaps consistently; see docs/ai-skill-routing-goal.md.",
1564
- " Development loop: observe route work explain briefly ask after remember policy.",
1565
- " For an already-allowed skill, disclose the selected skill at start and completion; do not ask for another approval.",
1566
- " 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].",
1567
- " If a policy-changing action is needed, pick one current action id from that brief and ask the user for one confirmation.",
1568
- " Apply one current action with skillboard apply-action <action-id> --config <path> --skills <dir> [--workflow <name>] --yes --json.",
1569
- " Read the returned post-apply brief, then run skillboard guard use automatically before invocation.",
1570
- " 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.",
1819
+ "v2 AI/automation control loop:",
1820
+ " Policy decides only enabled/disabled and per-skill opt-in sharing; see docs/ai-skill-routing-goal.md.",
1821
+ " Valid installed skills default to enabled and agent-local. Optional preference ranks only and never changes availability.",
1822
+ " Read brief --intent --agent <agent>, select an enabled installed skill, and run guard use automatically before use.",
1823
+ " For an allowed skill, work without another approval; ask once only before a persistent policy change.",
1824
+ " Source/provenance observations are audit metadata, never availability. Runtime/action authorization is outside SkillBoard.",
1825
+ " Stale v1 policy is read-only: preview with skillboard migrate v2 --config <path> --json, apply with --yes --json, or restore with --rollback <backup> --json.",
1571
1826
  ""
1572
1827
  ].join("\n");
1573
1828
  }
1574
1829
 
1575
1830
  function commandHelpText(command) {
1831
+ if (command === "skill") {
1832
+ return skillHelpText();
1833
+ }
1576
1834
  if (command === "brief") {
1577
1835
  return briefHelpText();
1578
1836
  }
@@ -1607,6 +1865,20 @@ function commandHelpText(command) {
1607
1865
  return usage === undefined ? null : genericCommandHelpText(usage);
1608
1866
  }
1609
1867
 
1868
+ function skillHelpText() {
1869
+ return [
1870
+ "Usage: skillboard skill enable|disable <skill-id> [--dry-run] [--json]",
1871
+ "Usage: skillboard skill share|unshare <skill-id> [--dry-run] [--json]",
1872
+ "Usage: skillboard skill preference <skill-id> --intent <term>[,<term>] --priority <integer> [--dry-run] [--json]",
1873
+ "Usage: skillboard skill forget <skill-id> [--dry-run] [--json]",
1874
+ "",
1875
+ "Policy records enablement and explicit per-skill sharing. Skills remain agent-local by default.",
1876
+ "Preference only ranks enabled skills installed for the current agent; it never changes availability.",
1877
+ "Forget removes policy only after an unshared skill is no longer present in generated inventory; it never deletes skill files.",
1878
+ ""
1879
+ ].join("\n");
1880
+ }
1881
+
1610
1882
  function genericCommandHelpText(usageLines) {
1611
1883
  return [
1612
1884
  ...usageLines.map((line) => `Usage: skillboard ${line}`),
@@ -1632,8 +1904,7 @@ function initHelpText() {
1632
1904
  "",
1633
1905
  "What changes:",
1634
1906
  " Writes skillboard.config.yaml, skills/, .skillboard/, AGENTS.md, and CLAUDE.md as needed.",
1635
- " Imports trusted user-local skills as on-request skills.",
1636
- " Keeps runtime, plugin, system, and external skills behind source review before manual activation.",
1907
+ " Scans installed skills into generated inventory without source-based authorization.",
1637
1908
  "",
1638
1909
  "Legacy next steps:",
1639
1910
  " Run skillboard doctor --summary.",
@@ -1644,7 +1915,7 @@ function initHelpText() {
1644
1915
 
1645
1916
  function setupHelpText() {
1646
1917
  return [
1647
- "Usage: skillboard setup [--yes] [--agent codex[,claude,opencode,hermes]]",
1918
+ "Usage: skillboard setup [--yes] [--agent codex[,claude,opencode,hermes]] [--skill-root <path>]",
1648
1919
  "",
1649
1920
  "Installs or refreshes SkillBoard at the agent layer, not into a project.",
1650
1921
  "A normal global package install already runs this automatically for detected supported agents.",
@@ -1655,8 +1926,10 @@ function setupHelpText() {
1655
1926
  "",
1656
1927
  "What changes after confirmation:",
1657
1928
  " Writes a SkillBoard guidance skill into detected user agent skill roots.",
1658
- " Does not write skillboard.config.yaml, .skillboard/, AGENTS.md, or CLAUDE.md in projects.",
1659
- " Teaches agents to use installed skills by default and resolve overlap by workflow priority.",
1929
+ " Creates ~/skillboard.config.yaml and ~/.skillboard/inventory.json; it writes no project files.",
1930
+ " Reconciles already-shared skills into agents and profiles added after the original share.",
1931
+ " --skill-root registers one custom root for the selected --agent and reuses it on future updates.",
1932
+ " Teaches agents to use local skills by default and share only user-selected skills.",
1660
1933
  " Remove this managed guidance later with skillboard uninstall --agent-layer.",
1661
1934
  "",
1662
1935
  "Supported agent homes:",
@@ -1669,6 +1942,7 @@ function setupHelpText() {
1669
1942
  " skillboard setup",
1670
1943
  " skillboard setup --yes",
1671
1944
  " skillboard setup --agent codex,claude,opencode,hermes --yes",
1945
+ " skillboard setup --agent hermes --skill-root ~/.hermes/profiles/work/skills --yes",
1672
1946
  ""
1673
1947
  ].join("\n");
1674
1948
  }
@@ -1701,14 +1975,17 @@ function importSkillHelpText() {
1701
1975
 
1702
1976
  function uninstallHelpText() {
1703
1977
  return [
1978
+ "Usage: skillboard uninstall --user (--dry-run|--yes) [--json]",
1704
1979
  "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]]",
1705
1980
  "",
1706
1981
  "This help is read-only. It does not load config or change project files.",
1707
1982
  "",
1708
- "Removes SkillBoard project bridge files, generated lifecycle scaffolding, or managed agent-layer guidance.",
1983
+ "Removes complete user-level SkillBoard state, project bridge files, generated lifecycle scaffolding, or managed agent-layer guidance.",
1709
1984
  "Default project cleanup removes SkillBoard settings and generated project state while preserving local skills and user-authored non-SkillBoard content.",
1710
1985
  "",
1711
1986
  "Options:",
1987
+ " --user Remove all marker-owned shared copies, managed guidance, home policy, and generated user state.",
1988
+ " --yes Confirm the user-level removal after previewing it.",
1712
1989
  " --dir <path> Project root to clean up; defaults to the current directory.",
1713
1990
  " --dry-run Preview the cleanup without writing changes.",
1714
1991
  " --keep-settings Preserve project SkillBoard settings and bridge guidance during default cleanup.",
@@ -1720,6 +1997,12 @@ function uninstallHelpText() {
1720
1997
  " --keep-empty-dirs Preserve empty generated directories.",
1721
1998
  " --agent-layer Remove managed user-agent skillboard guidance instead of project files.",
1722
1999
  " --agent <list> Target supported agents for --agent-layer cleanup.",
2000
+ " --json Print a machine-readable user-level removal plan or result.",
2001
+ "",
2002
+ "User-level cleanup:",
2003
+ " Run skillboard uninstall --user --dry-run, inspect the complete plan, then apply it with --yes.",
2004
+ " Removes only marker-owned shared copies and managed guidance; agent-owned and unmanaged skills are preserved.",
2005
+ " Removes ~/skillboard.config.yaml and ~/.skillboard only after managed copies and guidance are handled.",
1723
2006
  "",
1724
2007
  "Default project cleanup:",
1725
2008
  " Removes SkillBoard config, bridge blocks, and the entire .skillboard/ project state directory.",
@@ -1740,11 +2023,13 @@ function doctorHelpText() {
1740
2023
  return [
1741
2024
  "Usage: skillboard doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1742
2025
  "",
1743
- "Checks legacy local policy workspace health and source-review readiness.",
2026
+ "Checks the user-level policy and generated inventory health.",
2027
+ "Also compares the running package and PATH-selected SkillBoard executable.",
2028
+ "Installation discovery reads links and package metadata; it does not execute PATH candidates.",
1744
2029
  "The status command is an alias for doctor.",
1745
2030
  "",
1746
2031
  "Options:",
1747
- " --dir <path> Workspace root to check; defaults to the current directory.",
2032
+ " --dir <path> Advanced legacy workspace root override.",
1748
2033
  " --config <path> Use a specific skillboard.config.yaml.",
1749
2034
  " --skills <dir> Use a specific skills directory.",
1750
2035
  " --verify Verify local source/cache digests when available.",
@@ -1752,24 +2037,24 @@ function doctorHelpText() {
1752
2037
  " --summary Print a short human summary.",
1753
2038
  " --json Print an agent-readable payload.",
1754
2039
  "",
1755
- "Use this for existing workspaces that intentionally keep local SkillBoard policy files.",
1756
- "It is read-only and is not part of agent-layer setup.",
2040
+ "Without path overrides, this checks ~/skillboard.config.yaml and ~/.skillboard/inventory.json from any directory.",
2041
+ "Installation warnings are informational and do not change policy health.",
2042
+ "It is read-only.",
1757
2043
  ""
1758
2044
  ].join("\n");
1759
2045
  }
1760
2046
 
1761
2047
  function briefHelpText() {
1762
2048
  return [
1763
- "Usage: skillboard brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
2049
+ "Usage: skillboard brief [--agent codex|claude|opencode|hermes] [--intent <request>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1764
2050
  "",
1765
- "Reads the current SkillBoard brief without changing project files.",
2051
+ "Reads the current user-level SkillBoard brief without changing files.",
1766
2052
  "Use it when a user asks what skills the AI can use, a request has ambiguous skill overlap, or a policy change needs approval.",
1767
2053
  "",
1768
2054
  "Options:",
1769
- " --workflow <name> Evaluate one workflow.",
2055
+ " --agent <name> Evaluate actual installation for the current agent; required for v2 availability and routing.",
1770
2056
  " --intent <request> Add a natural-language request so SkillBoard can suggest a skill.",
1771
- " --dir <path> Use a project root; defaults to the current directory.",
1772
- " --config <path> Use a specific skillboard.config.yaml.",
2057
+ " --config <path> Advanced policy override; defaults to ~/skillboard.config.yaml.",
1773
2058
  " --skills <dir> Use a specific skills directory.",
1774
2059
  " --include-actions Include current action ids in JSON output.",
1775
2060
  " --verbose Show full lists instead of compact previews.",
@@ -1777,7 +2062,7 @@ function briefHelpText() {
1777
2062
  "",
1778
2063
  "AI use:",
1779
2064
  " Read this before answering availability questions.",
1780
- " Run skillboard guard use <skill-id> --workflow <name> before invoking a skill.",
2065
+ " Run skillboard guard use <skill-id> --agent <agent> before invoking a skill.",
1781
2066
  " If guard allows use, disclose the skill at the start and completion; do not ask for another approval.",
1782
2067
  " If a policy change is needed, ask the user to approve one current action id from this brief.",
1783
2068
  ""
@@ -1786,14 +2071,14 @@ function briefHelpText() {
1786
2071
 
1787
2072
  function routeHelpText() {
1788
2073
  return [
1789
- "Usage: skillboard route <intent> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2074
+ "Usage: skillboard route <intent> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1790
2075
  "",
1791
2076
  "Suggests the routed skill for a user request when several allowed skills may overlap.",
1792
2077
  "Use it when the AI needs a skill recommendation without changing policy.",
1793
2078
  "",
1794
2079
  "Options:",
1795
2080
  " <intent> Natural-language request, such as \"write tests first\".",
1796
- " --workflow <name> Workflow to route within.",
2081
+ " --agent <name> Route among skills installed for this agent.",
1797
2082
  " --config <path> Use a specific skillboard.config.yaml.",
1798
2083
  " --skills <dir> Use a specific skills directory.",
1799
2084
  " --json Print an agent-readable payload.",
@@ -1809,14 +2094,14 @@ function routeHelpText() {
1809
2094
 
1810
2095
  function canUseHelpText() {
1811
2096
  return [
1812
- "Usage: skillboard can-use <skill-id> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2097
+ "Usage: skillboard can-use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1813
2098
  "",
1814
- "Checks whether one skill is currently usable in a workflow without changing policy.",
2099
+ "Checks whether one skill is enabled and installed for the selected agent without changing policy.",
1815
2100
  "Use it when the AI needs an availability answer for a named skill.",
1816
2101
  "",
1817
2102
  "Options:",
1818
2103
  " <skill-id> Skill id to check.",
1819
- " --workflow <name> Workflow that would use the skill.",
2104
+ " --agent <name> Agent that would use the skill.",
1820
2105
  " --config <path> Use a specific skillboard.config.yaml.",
1821
2106
  " --skills <dir> Use a specific skills directory.",
1822
2107
  " --json Print an agent-readable payload.",
@@ -1831,7 +2116,7 @@ function canUseHelpText() {
1831
2116
 
1832
2117
  function guardHelpText() {
1833
2118
  return [
1834
- "Usage: skillboard guard use <skill-id> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
2119
+ "Usage: skillboard guard use <skill-id> --agent codex|claude|opencode|hermes [--config <path>] [--skills <dir>] [--json]",
1835
2120
  "",
1836
2121
  "Checks whether one skill may be used right now.",
1837
2122
  "Run this immediately before the AI invokes a skill.",
@@ -1839,7 +2124,7 @@ function guardHelpText() {
1839
2124
  "Options:",
1840
2125
  " use Guard a skill invocation.",
1841
2126
  " <skill-id> Skill id to check.",
1842
- " --workflow <name> Workflow that will use the skill.",
2127
+ " --agent <name> Agent that will use the skill.",
1843
2128
  " --config <path> Use a specific skillboard.config.yaml.",
1844
2129
  " --skills <dir> Use a specific skills directory.",
1845
2130
  " --json Print an agent-readable payload.",
@@ -1853,15 +2138,16 @@ function guardHelpText() {
1853
2138
 
1854
2139
  function applyActionHelpText() {
1855
2140
  return [
1856
- "Usage: skillboard apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
2141
+ "Usage: skillboard apply-action <action-id> [--agent codex|claude|opencode|hermes] [--intent <text>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1857
2142
  "",
1858
2143
  "Applies one current action id from the latest brief.",
1859
2144
  "Use it only after the user confirms a policy-changing action.",
1860
2145
  "",
1861
2146
  "Options:",
1862
2147
  " <action-id> One action id from the current brief.",
1863
- " --workflow <name> Workflow for workflow-scoped actions.",
1864
- " --dir <path> Project root; defaults to the current directory.",
2148
+ " --agent <name> Preserve the brief's current-agent context in preview and post-apply output.",
2149
+ " --intent <text> Intent bound to a current preference action.",
2150
+ " --dir <path> Advanced legacy workspace root override.",
1865
2151
  " --config <path> Use a specific skillboard.config.yaml.",
1866
2152
  " --skills <dir> Use a specific skills directory.",
1867
2153
  " --dry-run Preview the action without writing changes.",