agent-skillboard 0.2.18 → 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 (76) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +129 -258
  3. package/docs/adapters.md +37 -113
  4. package/docs/ai-skill-routing-goal.md +35 -109
  5. package/docs/capabilities.md +17 -104
  6. package/docs/install.md +36 -485
  7. package/docs/policy-model.md +50 -280
  8. package/docs/positioning.md +19 -86
  9. package/docs/profiles.md +21 -153
  10. package/docs/reference.md +115 -363
  11. package/docs/rollout-runbook.md +23 -25
  12. package/docs/routing.md +23 -90
  13. package/docs/user-flow.md +60 -284
  14. package/docs/value-proof.md +23 -181
  15. package/docs/variant-lifecycle.md +47 -67
  16. package/docs/versioning.md +31 -264
  17. package/examples/v2-multi-source.config.yaml +35 -0
  18. package/examples/v2-policy-error.config.yaml +6 -0
  19. package/package.json +1 -1
  20. package/src/advisor/actions.mjs +102 -6
  21. package/src/advisor/application-commands.mjs +10 -9
  22. package/src/advisor/apply-action.mjs +74 -1
  23. package/src/advisor/guidance.mjs +24 -16
  24. package/src/advisor/schema.mjs +17 -6
  25. package/src/advisor/skills.mjs +18 -5
  26. package/src/advisor.mjs +27 -9
  27. package/src/agent-integration-cli.mjs +13 -4
  28. package/src/agent-integration-content.mjs +21 -11
  29. package/src/agent-integration-files.mjs +1 -1
  30. package/src/agent-inventory-platforms.mjs +10 -0
  31. package/src/agent-inventory.mjs +23 -1
  32. package/src/agent-skill-import.mjs +2 -2
  33. package/src/audit-paths.mjs +42 -0
  34. package/src/brief-cli.mjs +3 -2
  35. package/src/brief-renderer.mjs +1 -0
  36. package/src/cli.mjs +498 -232
  37. package/src/compatibility.mjs +24 -0
  38. package/src/control/can-use-guard.mjs +21 -1
  39. package/src/control/config-write.mjs +32 -2
  40. package/src/control/skill-crud.mjs +5 -0
  41. package/src/control/v2-guard.mjs +175 -0
  42. package/src/control/v2-skill-crud.mjs +32 -0
  43. package/src/control/v2-skill-forget.mjs +38 -0
  44. package/src/control/variant-status.mjs +47 -1
  45. package/src/control.mjs +55 -0
  46. package/src/doctor.mjs +63 -4
  47. package/src/domain/v2-policy.mjs +111 -0
  48. package/src/hook-plan.mjs +33 -3
  49. package/src/impact.mjs +52 -29
  50. package/src/index.mjs +25 -1
  51. package/src/init.mjs +50 -34
  52. package/src/inventory-install-units.mjs +63 -0
  53. package/src/inventory-json.mjs +279 -0
  54. package/src/inventory-refresh.mjs +163 -18
  55. package/src/lifecycle-cli.mjs +40 -12
  56. package/src/lifecycle-content.mjs +52 -67
  57. package/src/migration/v1-to-v2.mjs +212 -0
  58. package/src/migration/v2-files.mjs +211 -0
  59. package/src/migration/v2-journal.mjs +169 -0
  60. package/src/migration/v2-projection.mjs +108 -0
  61. package/src/migration/v2-transaction.mjs +205 -0
  62. package/src/policy.mjs +3 -0
  63. package/src/reconcile.mjs +139 -111
  64. package/src/report.mjs +168 -148
  65. package/src/review.mjs +2 -0
  66. package/src/route-advisory.mjs +47 -2
  67. package/src/route-selection.mjs +38 -2
  68. package/src/route.mjs +62 -2
  69. package/src/shared-skill.mjs +301 -0
  70. package/src/source-digest.mjs +42 -0
  71. package/src/source-profiles.mjs +171 -144
  72. package/src/source-verification.mjs +32 -48
  73. package/src/uninstall.mjs +22 -0
  74. package/src/user-state-paths.mjs +19 -0
  75. package/src/user-uninstall.mjs +146 -0
  76. 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([
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);
@@ -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,28 +401,55 @@ 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
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,37 +1171,40 @@ async function remove(argv, options, stdout) {
983
1171
  }
984
1172
 
985
1173
  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>");
1174
+ assertKnownOptions("dashboard", options, new Set(["config", "skills", "out", "json"]));
1175
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1176
+ const markdown = renderDashboard(workspace);
1177
+ const out = options.get("out");
1178
+ if (out === undefined) {
1179
+ stdout.write(markdown);
1180
+ return 0;
1181
+ }
1182
+ await mkdir(dirname(out), { recursive: true });
1183
+ await writeFile(out, markdown, "utf8");
1184
+ stdout.write(`Dashboard written: ${out}\n`);
1185
+ return 0;
1186
+ }
1187
+
1188
+ async function reconcile(options, stdout) {
1189
+ assertKnownOptions("reconcile", options, new Set(["config", "skills", "actual-harnesses", "out", "json"]));
1190
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1191
+ const plan = reconcileWorkspace(workspace, { actualHarnesses: readCsv(options.get("actual-harnesses")) });
1192
+ const markdown = renderReconcilePlan(plan);
1193
+ const out = options.get("out");
1194
+ if (out === undefined) {
1195
+ stdout.write(markdown);
1196
+ return 0;
1197
+ }
1198
+ await mkdir(dirname(out), { recursive: true });
1199
+ await writeFile(out, markdown, "utf8");
1200
+ stdout.write(`Reconcile plan written: ${out}\n`);
1201
+ return 0;
1202
+ }
1203
+
1204
+ async function impact(argv, options, stdout) {
1205
+ assertKnownOptions("impact", options, new Set(["config", "skills", "out", "json"]));
1206
+ if (argv[0] !== "disable" || argv[1] === undefined) {
1207
+ throw new Error("Usage: skillboard impact disable <skill-id>");
1017
1208
  }
1018
1209
  const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
1019
1210
  const result = impactDisable(workspace, argv[1]);
@@ -1023,22 +1214,22 @@ async function impact(argv, options, stdout) {
1023
1214
  }
1024
1215
  const markdown = renderImpact(result);
1025
1216
  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
-
1217
+ if (out === undefined) {
1218
+ stdout.write(markdown);
1219
+ return 0;
1220
+ }
1221
+ await mkdir(dirname(out), { recursive: true });
1222
+ await writeFile(out, markdown, "utf8");
1223
+ stdout.write(`Impact report written: ${out}\n`);
1224
+ return 0;
1225
+ }
1226
+
1036
1227
  function renderImpact(result) {
1037
- return [
1038
- `# Disable Impact: ${result.skillId}`,
1039
- "",
1040
- `- Found: \`${result.exists}\``,
1041
- `- Risk: ${result.risk}`,
1228
+ return [
1229
+ `# Disable Impact: ${result.skillId}`,
1230
+ "",
1231
+ `- Found: \`${result.exists}\``,
1232
+ `- Risk: ${result.risk}`,
1042
1233
  `- Affected workflows: ${formatList(result.affectedWorkflows)}`,
1043
1234
  `- Affected required outputs: ${formatList(result.affectedOutputs)}`,
1044
1235
  `- Alternatives: ${formatList(result.alternatives)}`,
@@ -1058,41 +1249,74 @@ 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;
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
-
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(", ")}.`);
1280
+ }
1281
+ }
1282
+
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) {
1293
+ const options = new Map();
1294
+ for (let index = 0; index < args.length; index += 1) {
1295
+ const arg = args[index];
1296
+ if (!arg.startsWith("--")) {
1297
+ continue;
1298
+ }
1299
+ const key = arg.slice(2);
1300
+ const value = args[index + 1];
1301
+ if (value === undefined || value.startsWith("--")) {
1302
+ if (VALUE_OPTIONS.has(key)) throw new Error(`Missing value for ${command} option: --${key}`);
1303
+ options.set(key, "true");
1304
+ continue;
1305
+ }
1306
+ options.set(key, value);
1307
+ index += 1;
1308
+ }
1309
+ return options;
1310
+ }
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
  }
@@ -1105,7 +1329,7 @@ function readCsv(value) {
1105
1329
  if (value === undefined || value.trim() === "") {
1106
1330
  return [];
1107
1331
  }
1108
- return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1332
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1109
1333
  }
1110
1334
 
1111
1335
  function renderImportMerge(result) {
@@ -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,68 +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
- " 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]",
1520
1780
  " 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
1781
  " doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1524
1782
  " 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>",
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]",
1530
1790
  " 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]",
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]",
1557
1796
  "",
1558
- "Legacy project policy mode:",
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:",
1559
1802
  " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1560
1803
  " Deprecated project-local policy bootstrap; not needed for normal use.",
1561
1804
  "",
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.",
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.",
1571
1812
  ""
1572
1813
  ].join("\n");
1573
1814
  }
1574
1815
 
1575
1816
  function commandHelpText(command) {
1817
+ if (command === "skill") {
1818
+ return skillHelpText();
1819
+ }
1576
1820
  if (command === "brief") {
1577
1821
  return briefHelpText();
1578
1822
  }
@@ -1607,6 +1851,20 @@ function commandHelpText(command) {
1607
1851
  return usage === undefined ? null : genericCommandHelpText(usage);
1608
1852
  }
1609
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
+
1610
1868
  function genericCommandHelpText(usageLines) {
1611
1869
  return [
1612
1870
  ...usageLines.map((line) => `Usage: skillboard ${line}`),
@@ -1632,8 +1890,7 @@ function initHelpText() {
1632
1890
  "",
1633
1891
  "What changes:",
1634
1892
  " 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.",
1893
+ " Scans installed skills into generated inventory without source-based authorization.",
1637
1894
  "",
1638
1895
  "Legacy next steps:",
1639
1896
  " Run skillboard doctor --summary.",
@@ -1655,8 +1912,8 @@ function setupHelpText() {
1655
1912
  "",
1656
1913
  "What changes after confirmation:",
1657
1914
  " 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.",
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.",
1660
1917
  " Remove this managed guidance later with skillboard uninstall --agent-layer.",
1661
1918
  "",
1662
1919
  "Supported agent homes:",
@@ -1701,14 +1958,17 @@ function importSkillHelpText() {
1701
1958
 
1702
1959
  function uninstallHelpText() {
1703
1960
  return [
1961
+ "Usage: skillboard uninstall --user (--dry-run|--yes) [--json]",
1704
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]]",
1705
1963
  "",
1706
1964
  "This help is read-only. It does not load config or change project files.",
1707
1965
  "",
1708
- "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.",
1709
1967
  "Default project cleanup removes SkillBoard settings and generated project state while preserving local skills and user-authored non-SkillBoard content.",
1710
1968
  "",
1711
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.",
1712
1972
  " --dir <path> Project root to clean up; defaults to the current directory.",
1713
1973
  " --dry-run Preview the cleanup without writing changes.",
1714
1974
  " --keep-settings Preserve project SkillBoard settings and bridge guidance during default cleanup.",
@@ -1720,6 +1980,12 @@ function uninstallHelpText() {
1720
1980
  " --keep-empty-dirs Preserve empty generated directories.",
1721
1981
  " --agent-layer Remove managed user-agent skillboard guidance instead of project files.",
1722
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.",
1723
1989
  "",
1724
1990
  "Default project cleanup:",
1725
1991
  " Removes SkillBoard config, bridge blocks, and the entire .skillboard/ project state directory.",
@@ -1740,11 +2006,11 @@ function doctorHelpText() {
1740
2006
  return [
1741
2007
  "Usage: skillboard doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1742
2008
  "",
1743
- "Checks legacy local policy workspace health and source-review readiness.",
2009
+ "Checks the user-level policy and generated inventory health.",
1744
2010
  "The status command is an alias for doctor.",
1745
2011
  "",
1746
2012
  "Options:",
1747
- " --dir <path> Workspace root to check; defaults to the current directory.",
2013
+ " --dir <path> Advanced legacy workspace root override.",
1748
2014
  " --config <path> Use a specific skillboard.config.yaml.",
1749
2015
  " --skills <dir> Use a specific skills directory.",
1750
2016
  " --verify Verify local source/cache digests when available.",
@@ -1752,24 +2018,23 @@ function doctorHelpText() {
1752
2018
  " --summary Print a short human summary.",
1753
2019
  " --json Print an agent-readable payload.",
1754
2020
  "",
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.",
2021
+ "Without path overrides, this checks ~/skillboard.config.yaml and ~/.skillboard/inventory.json from any directory.",
2022
+ "It is read-only.",
1757
2023
  ""
1758
2024
  ].join("\n");
1759
2025
  }
1760
2026
 
1761
2027
  function briefHelpText() {
1762
2028
  return [
1763
- "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]",
1764
2030
  "",
1765
- "Reads the current SkillBoard brief without changing project files.",
2031
+ "Reads the current user-level SkillBoard brief without changing files.",
1766
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.",
1767
2033
  "",
1768
2034
  "Options:",
1769
- " --workflow <name> Evaluate one workflow.",
2035
+ " --agent <name> Evaluate actual installation for the current agent; required for v2 availability and routing.",
1770
2036
  " --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.",
2037
+ " --config <path> Advanced policy override; defaults to ~/skillboard.config.yaml.",
1773
2038
  " --skills <dir> Use a specific skills directory.",
1774
2039
  " --include-actions Include current action ids in JSON output.",
1775
2040
  " --verbose Show full lists instead of compact previews.",
@@ -1777,7 +2042,7 @@ function briefHelpText() {
1777
2042
  "",
1778
2043
  "AI use:",
1779
2044
  " Read this before answering availability questions.",
1780
- " 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.",
1781
2046
  " If guard allows use, disclose the skill at the start and completion; do not ask for another approval.",
1782
2047
  " If a policy change is needed, ask the user to approve one current action id from this brief.",
1783
2048
  ""
@@ -1786,14 +2051,14 @@ function briefHelpText() {
1786
2051
 
1787
2052
  function routeHelpText() {
1788
2053
  return [
1789
- "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]",
1790
2055
  "",
1791
2056
  "Suggests the routed skill for a user request when several allowed skills may overlap.",
1792
2057
  "Use it when the AI needs a skill recommendation without changing policy.",
1793
2058
  "",
1794
2059
  "Options:",
1795
2060
  " <intent> Natural-language request, such as \"write tests first\".",
1796
- " --workflow <name> Workflow to route within.",
2061
+ " --agent <name> Route among skills installed for this agent.",
1797
2062
  " --config <path> Use a specific skillboard.config.yaml.",
1798
2063
  " --skills <dir> Use a specific skills directory.",
1799
2064
  " --json Print an agent-readable payload.",
@@ -1809,14 +2074,14 @@ function routeHelpText() {
1809
2074
 
1810
2075
  function canUseHelpText() {
1811
2076
  return [
1812
- "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]",
1813
2078
  "",
1814
- "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.",
1815
2080
  "Use it when the AI needs an availability answer for a named skill.",
1816
2081
  "",
1817
2082
  "Options:",
1818
2083
  " <skill-id> Skill id to check.",
1819
- " --workflow <name> Workflow that would use the skill.",
2084
+ " --agent <name> Agent that would use the skill.",
1820
2085
  " --config <path> Use a specific skillboard.config.yaml.",
1821
2086
  " --skills <dir> Use a specific skills directory.",
1822
2087
  " --json Print an agent-readable payload.",
@@ -1831,7 +2096,7 @@ function canUseHelpText() {
1831
2096
 
1832
2097
  function guardHelpText() {
1833
2098
  return [
1834
- "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]",
1835
2100
  "",
1836
2101
  "Checks whether one skill may be used right now.",
1837
2102
  "Run this immediately before the AI invokes a skill.",
@@ -1839,7 +2104,7 @@ function guardHelpText() {
1839
2104
  "Options:",
1840
2105
  " use Guard a skill invocation.",
1841
2106
  " <skill-id> Skill id to check.",
1842
- " --workflow <name> Workflow that will use the skill.",
2107
+ " --agent <name> Agent that will use the skill.",
1843
2108
  " --config <path> Use a specific skillboard.config.yaml.",
1844
2109
  " --skills <dir> Use a specific skills directory.",
1845
2110
  " --json Print an agent-readable payload.",
@@ -1853,15 +2118,16 @@ function guardHelpText() {
1853
2118
 
1854
2119
  function applyActionHelpText() {
1855
2120
  return [
1856
- "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]",
1857
2122
  "",
1858
2123
  "Applies one current action id from the latest brief.",
1859
2124
  "Use it only after the user confirms a policy-changing action.",
1860
2125
  "",
1861
2126
  "Options:",
1862
2127
  " <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.",
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.",
1865
2131
  " --config <path> Use a specific skillboard.config.yaml.",
1866
2132
  " --skills <dir> Use a specific skills directory.",
1867
2133
  " --dry-run Preview the action without writing changes.",