agent-skillboard 0.1.1 → 0.2.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 (58) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +154 -630
  3. package/docs/adapters.md +96 -96
  4. package/docs/ai-skill-routing-goal.md +112 -0
  5. package/docs/capabilities.md +6 -0
  6. package/docs/install.md +155 -105
  7. package/docs/plans/skillboard-variant-lifecycle-handoff.md +56 -0
  8. package/docs/policy-model.md +266 -214
  9. package/docs/positioning.md +94 -94
  10. package/docs/reference.md +349 -0
  11. package/docs/routing.md +85 -0
  12. package/docs/user-flow.md +75 -16
  13. package/docs/value-proof.md +190 -0
  14. package/docs/variant-lifecycle.md +86 -0
  15. package/docs/versioning.md +157 -138
  16. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -8
  17. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -8
  18. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -8
  19. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -8
  20. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -8
  21. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -8
  22. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -8
  23. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -8
  24. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -8
  25. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -8
  26. package/examples/skills/grill-me/SKILL.md +9 -9
  27. package/examples/skills/grill-with-docs/SKILL.md +9 -9
  28. package/examples/skills/requirement-intake/SKILL.md +9 -9
  29. package/examples/skills/tdd/SKILL.md +8 -8
  30. package/package.json +25 -20
  31. package/src/advisor/guidance.mjs +232 -0
  32. package/src/advisor/schema.mjs +2 -0
  33. package/src/advisor/skills.mjs +2 -0
  34. package/src/advisor.mjs +36 -7
  35. package/src/brief-cli.mjs +6 -5
  36. package/src/brief-renderer.mjs +225 -8
  37. package/src/cli.mjs +589 -34
  38. package/src/config-helpers.mjs +34 -18
  39. package/src/conflicts.mjs +70 -0
  40. package/src/control/can-use-guard.mjs +8 -3
  41. package/src/control/skill-crud.mjs +142 -0
  42. package/src/control/skill-variants.mjs +221 -0
  43. package/src/control/source-trust.mjs +1 -0
  44. package/src/control/variant-files.mjs +265 -0
  45. package/src/control/variant-lifecycle-config.mjs +156 -0
  46. package/src/control/variant-reset.mjs +171 -0
  47. package/src/control/variant-status.mjs +75 -0
  48. package/src/control.mjs +13 -1
  49. package/src/domain/rules/skills.mjs +60 -0
  50. package/src/domain/rules/workflows.mjs +13 -0
  51. package/src/impact.mjs +21 -12
  52. package/src/index.mjs +13 -1
  53. package/src/lifecycle-cli.mjs +18 -7
  54. package/src/lifecycle-content.mjs +29 -22
  55. package/src/route.mjs +537 -0
  56. package/src/source-verification.mjs +7 -3
  57. package/src/workspace.mjs +141 -43
  58. package/tsconfig.lsp.json +1 -1
package/src/cli.mjs CHANGED
@@ -1,11 +1,15 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute } from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import YAML from "yaml";
4
6
  import {
5
7
  activateSkill,
6
8
  addHarness,
7
9
  addSkill,
10
+ addSkillVariant,
8
11
  addWorkflow,
12
+ approveSkillVariant,
9
13
  auditSources,
10
14
  blockSkill,
11
15
  canUseSkill,
@@ -13,6 +17,7 @@ import {
13
17
  detectInstallOutput,
14
18
  doctorProject,
15
19
  explainSkill,
20
+ forkSkillVariant,
16
21
  importSource,
17
22
  installGuardHook,
18
23
  impactDisable,
@@ -26,6 +31,7 @@ import {
26
31
  preferSkill,
27
32
  quarantineSkill,
28
33
  removeSkill,
34
+ resetSkillVariant,
29
35
  reconcileWorkspace,
30
36
  refreshAgentInventory,
31
37
  refreshSourcePins,
@@ -38,7 +44,9 @@ import {
38
44
  rolloutPlan,
39
45
  rolloutReport,
40
46
  rolloutRollback,
47
+ routeSkill,
41
48
  verifySources,
49
+ variantLifecycleStatus,
42
50
  writeLockfile
43
51
  } from "./index.mjs";
44
52
  // SIZE_OK: src/cli.mjs is pre-existing command-router debt; brief behavior delegates to src/brief-cli.mjs and hook planning delegates through src/hook-plan.mjs until a broader router split.
@@ -49,21 +57,65 @@ import { planGuardHookInstall } from "./control.mjs";
49
57
  import { writeCheckedConfig } from "./control/config-write.mjs";
50
58
  import { runInitCommand, runUninstallCommand } from "./lifecycle-cli.mjs";
51
59
 
60
+ const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
61
+
52
62
  const APPLY_ACTION_VALUE_OPTIONS = new Set(["workflow", "dir", "config", "skills", "out", "skillboard-bin"]);
53
-
54
- export async function main(argv, stdout, stderr) {
63
+ const COMMAND_USAGE = new Map([
64
+ ["uninstall", ["uninstall [--dir <path>] [--dry-run] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs]"]],
65
+ [
66
+ "inventory",
67
+ [
68
+ "inventory refresh [--dir <path>] [--config <path>] [--scan-root <dir>[,<dir>]] [--dry-run] [--json]",
69
+ "inventory detect --unit <id> --config <path> [--install-output <path>] [--config-file a,b] [--source <value>] [--kind <kind>] [--scope <scope>] [--dry-run] [--json]"
70
+ ]
71
+ ],
72
+ ["sources", ["sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]"]],
73
+ ["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]"]],
74
+ ["scan", ["scan --config <path>"]],
75
+ ["check", ["check --config <path> --skills <dir>"]],
76
+ ["list", ["list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]"]],
77
+ ["explain", ["explain <skill-id> --config <path> --skills <dir> [--json]"]],
78
+ ["can-use", ["can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]"]],
79
+ ["audit", ["audit sources --config <path> --skills <dir> [--verify] [--json]"]],
80
+ ["rollout", ["rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]"]],
81
+ ["hook", ["hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]"]],
82
+ ["lock", ["lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]"]],
83
+ ["review", ["review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]"]],
84
+ ["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]"]],
85
+ ["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]"]],
86
+ ["activate", ["activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]"]],
87
+ ["block", ["block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]"]],
88
+ ["quarantine", ["quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]"]],
89
+ ["prefer", ["prefer <skill-id> --workflow <name> --capability <name> --config <path> --skills <dir> [--dry-run] [--json]"]],
90
+ ["remove", ["remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]"]],
91
+ ["dashboard", ["dashboard --config <path> --skills <dir> [--out <path>]"]],
92
+ ["reconcile", ["reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]"]],
93
+ ["impact", ["impact disable <skill-id> --config <path> --skills <dir> [--out <path>] [--json]"]]
94
+ ]);
95
+
96
+ if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
97
+ process.exitCode = await main(process.argv.slice(2), process.stdout, process.stderr);
98
+ }
99
+
100
+ export async function main(argv, stdout, stderr) {
55
101
  try {
56
102
  return await run(argv, stdout, stderr);
57
103
  } catch (error) {
58
104
  stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
59
- return 1;
105
+ return Number.isInteger(error?.exitCode) ? error.exitCode : 1;
60
106
  }
61
107
  }
62
108
 
63
- async function run(argv, stdout, stderr) {
64
- const command = argv[0] ?? "help";
65
- const options = parseOptions(argv.slice(1));
66
- switch (command) {
109
+ async function run(argv, stdout, stderr) {
110
+ const command = argv[0] ?? "help";
111
+ const commandArgs = argv.slice(1);
112
+ const options = parseOptions(commandArgs);
113
+ const help = selectedHelpText(command, commandArgs, options);
114
+ if (help !== null) {
115
+ stdout.write(help);
116
+ return 0;
117
+ }
118
+ switch (command) {
67
119
  case "init":
68
120
  return await runInitCommand(options, stdout);
69
121
  case "uninstall":
@@ -92,6 +144,8 @@ async function run(argv, stdout, stderr) {
92
144
  return await list(argv.slice(1), options, stdout);
93
145
  case "explain":
94
146
  return await explain(argv.slice(1), options, stdout);
147
+ case "route":
148
+ return await route(argv.slice(1), options, stdout);
95
149
  case "can-use":
96
150
  return await canUse(argv.slice(1), options, stdout);
97
151
  case "guard":
@@ -110,6 +164,8 @@ async function run(argv, stdout, stderr) {
110
164
  return await activate(argv.slice(1), options, stdout);
111
165
  case "add":
112
166
  return await add(argv.slice(1), options, stdout);
167
+ case "variant":
168
+ return await variant(argv.slice(1), options, stdout);
113
169
  case "block":
114
170
  return await block(argv.slice(1), options, stdout);
115
171
  case "quarantine":
@@ -124,17 +180,34 @@ async function run(argv, stdout, stderr) {
124
180
  return await reconcile(options, stdout);
125
181
  case "impact":
126
182
  return await impact(argv.slice(1), options, stdout);
127
- case "help":
128
- case "--help":
129
- case "-h":
130
- stdout.write(helpText());
131
- return 0;
132
- default:
133
- stderr.write(`Unknown command: ${command}\n`);
134
- stdout.write(helpText());
135
- return 1;
136
- }
137
- }
183
+ case "help":
184
+ case "--help":
185
+ case "-h":
186
+ stdout.write(helpText());
187
+ return 0;
188
+ case "--version":
189
+ case "-v":
190
+ stdout.write(`${VERSION}\n`);
191
+ return 0;
192
+ default:
193
+ stderr.write(`Unknown command: ${command}\nRun skillboard help for usage.\n`);
194
+ return 1;
195
+ }
196
+ }
197
+
198
+ function selectedHelpText(command, args, options) {
199
+ if (command === "help") {
200
+ const topic = positionalArgs(args)[0];
201
+ return topic === undefined ? helpText() : commandHelpText(topic) ?? helpText();
202
+ }
203
+ if (command === "--help" || command === "-h") {
204
+ return helpText();
205
+ }
206
+ if (options.get("help") === "true" || args.includes("-h")) {
207
+ return commandHelpText(command);
208
+ }
209
+ return null;
210
+ }
138
211
 
139
212
  async function importProfile(options, stdout) {
140
213
  const profileRef = options.get("profile");
@@ -326,6 +399,25 @@ async function explain(argv, options, stdout) {
326
399
  return 0;
327
400
  }
328
401
 
402
+ async function route(argv, options, stdout) {
403
+ const intent = positionalArgs(argv).join(" ").trim();
404
+ const workflow = options.get("workflow");
405
+ if (intent.length === 0 || workflow === undefined) {
406
+ throw new Error("Usage: skillboard route <intent> --workflow <name>");
407
+ }
408
+ const config = configPath(options);
409
+ const skills = skillsRoot(options);
410
+ const workspace = await loadWorkspace({ configPath: config, skillsRoot: skills });
411
+ const result = routeSkill(workspace, {
412
+ intent,
413
+ workflow,
414
+ configPath: config,
415
+ skillsRoot: skills
416
+ });
417
+ writeOutput(stdout, result, options, () => renderRoute(result));
418
+ return 0;
419
+ }
420
+
329
421
  async function canUse(argv, options, stdout) {
330
422
  const skillId = positionalArgs(argv)[0];
331
423
  const workflow = options.get("workflow");
@@ -578,6 +670,219 @@ async function addHarnessCommand(args, options, stdout) {
578
670
  return 0;
579
671
  }
580
672
 
673
+ async function variant(argv, options, stdout) {
674
+ try {
675
+ return await runVariant(argv, options, stdout);
676
+ } catch (error) {
677
+ const payload = variantErrorPayload(error);
678
+ if (options.get("json") === "true") {
679
+ stdout.write(`${JSON.stringify({ ok: false, error: payload.error }, null, 2)}\n`);
680
+ return payload.exitCode;
681
+ }
682
+ throw Object.assign(new Error(payload.error.message), { exitCode: payload.exitCode });
683
+ }
684
+ }
685
+
686
+ async function runVariant(argv, options, stdout) {
687
+ const args = positionalArgs(argv);
688
+ const subcommand = args[0];
689
+ if (subcommand === "add") {
690
+ return await variantAddCommand(args, options, stdout);
691
+ }
692
+ if (subcommand === "fork") {
693
+ return await variantForkCommand(args, options, stdout);
694
+ }
695
+ if (subcommand === "status") {
696
+ return await variantStatusCommand(args, options, stdout);
697
+ }
698
+ if (subcommand === "approve") {
699
+ return await variantApproveCommand(args, options, stdout);
700
+ }
701
+ if (subcommand === "reset") {
702
+ return await variantResetCommand(args, options, stdout);
703
+ }
704
+ throw new VariantCliError("unknown_variant_subcommand", `Unknown variant subcommand: ${subcommand ?? "<missing>"}`, 2);
705
+ }
706
+
707
+ async function variantAddCommand(args, options, stdout) {
708
+ const variantId = args[1];
709
+ const baseId = options.get("from");
710
+ const capability = options.get("capability");
711
+ const workflow = options.get("workflow");
712
+ if (variantId === undefined || baseId === undefined || capability === undefined || workflow === undefined) {
713
+ throw new VariantCliError("usage_error", variantAddUsage(), 2);
714
+ }
715
+ const result = await addSkillVariant({
716
+ variantId,
717
+ baseId,
718
+ capability,
719
+ workflow,
720
+ path: options.get("path"),
721
+ mode: options.get("mode"),
722
+ category: options.get("category"),
723
+ ownerInstallUnit: options.get("owner-install-unit"),
724
+ configPath: configPath(options),
725
+ skillsRoot: skillsRoot(options),
726
+ dryRun: options.get("dry-run") === "true"
727
+ });
728
+ writeControlResult(stdout, result, options);
729
+ return 0;
730
+ }
731
+
732
+ async function variantForkCommand(args, options, stdout) {
733
+ const variantId = args[1];
734
+ const baseId = options.get("from");
735
+ const capability = options.get("capability");
736
+ const workflow = options.get("workflow");
737
+ if (variantId === undefined || baseId === undefined || capability === undefined || workflow === undefined || options.get("path") === undefined) {
738
+ throw new VariantCliError("usage_error", variantForkUsage(), 2);
739
+ }
740
+ const result = await forkSkillVariant({
741
+ variantId,
742
+ baseId,
743
+ capability,
744
+ workflow,
745
+ path: options.get("path"),
746
+ adaptedFor: options.get("adapted-for"),
747
+ category: options.get("category"),
748
+ ownerInstallUnit: options.get("owner-install-unit"),
749
+ configPath: configPath(options),
750
+ skillsRoot: skillsRoot(options),
751
+ dryRun: options.get("dry-run") === "true"
752
+ });
753
+ writeLifecycleResult(stdout, result, options);
754
+ return 0;
755
+ }
756
+
757
+ async function variantStatusCommand(args, options, stdout) {
758
+ const variantId = args[1];
759
+ if (variantId === undefined) {
760
+ throw new VariantCliError("usage_error", "Usage: skillboard variant status <variant-id> --config <path> --skills <dir> [--json]", 2);
761
+ }
762
+ const result = await variantLifecycleStatus({ variantId, configPath: configPath(options), skillsRoot: skillsRoot(options) });
763
+ writeOutput(stdout, result, options, () => renderVariantStatus(result));
764
+ return 0;
765
+ }
766
+
767
+ async function variantApproveCommand(args, options, stdout) {
768
+ const variantId = args[1];
769
+ if (variantId === undefined) {
770
+ throw new VariantCliError("usage_error", "Usage: skillboard variant approve <variant-id> --config <path> --skills <dir> [--mode manual-only|router-only|workflow-auto] [--dry-run] [--json]", 2);
771
+ }
772
+ const result = await approveSkillVariant({
773
+ variantId,
774
+ mode: options.get("mode"),
775
+ configPath: configPath(options),
776
+ skillsRoot: skillsRoot(options),
777
+ dryRun: options.get("dry-run") === "true"
778
+ });
779
+ writeLifecycleResult(stdout, result, options);
780
+ return 0;
781
+ }
782
+
783
+ async function variantResetCommand(args, options, stdout) {
784
+ const variantId = args[1];
785
+ if (variantId === undefined) {
786
+ throw new VariantCliError("usage_error", "Usage: skillboard variant reset <variant-id> --to-base|--to-approved --config <path> --skills <dir> [--yes] [--dry-run] [--mode manual-only|router-only|workflow-auto] [--json]", 2);
787
+ }
788
+ const result = await resetSkillVariant({
789
+ variantId,
790
+ toBase: options.get("to-base") === "true",
791
+ toApproved: options.get("to-approved") === "true",
792
+ mode: options.get("mode"),
793
+ yes: options.get("yes") === "true",
794
+ dryRun: options.get("dry-run") === "true",
795
+ configPath: configPath(options),
796
+ skillsRoot: skillsRoot(options)
797
+ });
798
+ writeLifecycleResult(stdout, result, options);
799
+ return 0;
800
+ }
801
+
802
+ function variantAddUsage() {
803
+ return "Usage: skillboard 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]";
804
+ }
805
+
806
+ function variantForkUsage() {
807
+ return "Usage: skillboard 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]";
808
+ }
809
+
810
+ class VariantCliError extends Error {
811
+ constructor(code, message, exitCode) {
812
+ super(message);
813
+ this.code = code;
814
+ this.exitCode = exitCode;
815
+ }
816
+ }
817
+
818
+ function variantErrorPayload(error) {
819
+ if (error instanceof VariantCliError) {
820
+ return { exitCode: error.exitCode, error: { code: error.code, message: error.message } };
821
+ }
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ if (
824
+ message.startsWith("Usage:")
825
+ || message.includes("requires exactly one of --to-base or --to-approved")
826
+ || message.includes("requires --mode manual-only")
827
+ ) {
828
+ return { exitCode: 2, error: { code: "usage_error", message } };
829
+ }
830
+ if (message.includes("requires --yes")) {
831
+ return { exitCode: 1, error: { code: "confirmation_required", message } };
832
+ }
833
+ if (message.toLowerCase().includes("snapshot")) {
834
+ return { exitCode: 1, error: { code: "snapshot_error", message } };
835
+ }
836
+ if (message.toLowerCase().includes("symlink") || message.toLowerCase().includes("path")) {
837
+ return { exitCode: 1, error: { code: "path_error", message } };
838
+ }
839
+ return { exitCode: 1, error: { code: "lifecycle_error", message } };
840
+ }
841
+
842
+ function writeLifecycleResult(stdout, result, options) {
843
+ const warnings = result.warnings ?? result.policy?.warnings ?? [];
844
+ if (options.get("json") === "true") {
845
+ stdout.write(`${JSON.stringify({
846
+ message: result.message,
847
+ dryRun: result.dryRun,
848
+ changed: result.changed,
849
+ plan: result.plan,
850
+ filePlan: result.filePlan ?? [],
851
+ skill: result.skill,
852
+ variant: result.variant,
853
+ warnings
854
+ }, null, 2)}\n`);
855
+ return;
856
+ }
857
+ stdout.write(`${result.dryRun ? "Dry run: " : ""}${result.message}\n`);
858
+ stdout.write(renderChangePlan(result.plan));
859
+ stdout.write(renderFilePlan(result.filePlan ?? []));
860
+ if (warnings.length > 0) {
861
+ stdout.write(`${warnings.join("\n")}\n`);
862
+ }
863
+ }
864
+
865
+ function renderFilePlan(filePlan) {
866
+ if (filePlan.length === 0) {
867
+ return "File operations: none\n";
868
+ }
869
+ return `File operations:\n${filePlan.map((item) => `- ${item.operation ?? "write"} ${item.path ?? item.target ?? "unknown"}`).join("\n")}\n`;
870
+ }
871
+
872
+ function renderVariantStatus(result) {
873
+ return [
874
+ `Variant ${result.skill}: ${result.computedStatus}`,
875
+ `Live digest: ${result.liveDigest ?? "missing"}`,
876
+ `Base digest: ${result.baseDigest}`,
877
+ `Approved digest: ${result.approvedDigest ?? "none"}`,
878
+ `Live file: ${result.files.live.path}`,
879
+ `Base snapshot: ${result.files.baseSnapshot.path}`,
880
+ `Approved snapshot: ${result.files.approvedSnapshot?.path ?? "none"}`,
881
+ `Warnings: ${result.warnings.length === 0 ? "none" : result.warnings.join("; ")}`,
882
+ ""
883
+ ].join("\n");
884
+ }
885
+
581
886
  async function block(argv, options, stdout) {
582
887
  const skillId = positionalArgs(argv)[0];
583
888
  const workflow = options.get("workflow");
@@ -702,13 +1007,24 @@ function renderImpact(result) {
702
1007
  "",
703
1008
  `- Found: \`${result.exists}\``,
704
1009
  `- Risk: ${result.risk}`,
705
- `- Affected workflows: ${formatList(result.affectedWorkflows)}`,
706
- `- Affected required outputs: ${formatList(result.affectedOutputs)}`,
707
- `- Alternatives: ${formatList(result.alternatives)}`,
708
- ""
709
- ].join("\n");
710
- }
711
-
1010
+ `- Affected workflows: ${formatList(result.affectedWorkflows)}`,
1011
+ `- Affected required outputs: ${formatList(result.affectedOutputs)}`,
1012
+ `- Alternatives: ${formatList(result.alternatives)}`,
1013
+ `- Conflicting skills: ${formatList(result.conflictingSkills ?? [])}`,
1014
+ `- Active conflicts: ${formatActiveConflicts(result.activeConflicts ?? [])}`,
1015
+ ""
1016
+ ].join("\n");
1017
+ }
1018
+
1019
+ function formatActiveConflicts(entries) {
1020
+ if (entries.length === 0) {
1021
+ return "none";
1022
+ }
1023
+ return entries
1024
+ .map((entry) => `\`${entry.workflow}:${entry.skill}<->${entry.conflictingSkill}\``)
1025
+ .join(", ");
1026
+ }
1027
+
712
1028
  function configPath(options) {
713
1029
  return options.get("config") ?? "skillboard.config.yaml";
714
1030
  }
@@ -1012,6 +1328,63 @@ function renderCanUse(result) {
1012
1328
  return lines.join("\n");
1013
1329
  }
1014
1330
 
1331
+ function renderRoute(result) {
1332
+ const lines = [
1333
+ `Intent: ${result.intent}`,
1334
+ `Workflow: ${result.workflow}`,
1335
+ `Match source: ${result.match_source}`,
1336
+ `Matched capability: ${result.matched_capability ?? "none"}`,
1337
+ `Matched skill: ${result.matched_skill ?? "none"}`,
1338
+ `Confidence: ${result.confidence}`,
1339
+ `Why: ${result.recommendation_reason}`,
1340
+ `Matched terms: ${formatList(result.matched_terms)}`,
1341
+ `Recommended skill: ${result.recommended_skill ?? "none"}`,
1342
+ `Fallback skills: ${formatList(result.fallback_skills)}`
1343
+ ];
1344
+ if ((result.route_candidates ?? []).length > 0) {
1345
+ lines.push("Route candidates:");
1346
+ for (const candidate of result.route_candidates) {
1347
+ lines.push(`- ${candidate.skill} (${routeCandidateStatus(candidate)})`);
1348
+ if (!candidate.guard_allowed && candidate.guard_reasons.length > 0) {
1349
+ lines.push(` - ${candidate.guard_reasons[0]}`);
1350
+ }
1351
+ }
1352
+ }
1353
+ if (result.guard_command !== null) {
1354
+ lines.push(`Guard: ${result.guard_command}`);
1355
+ }
1356
+ if (result.usage_disclosure !== null && result.usage_disclosure !== undefined) {
1357
+ lines.push(`Disclosure: run the guard automatically, state at the start that ${result.recommended_skill} is being used, and state at completion that it was used. No extra user approval is needed when the guard allows it.`);
1358
+ lines.push(`Say before use: "${result.usage_disclosure.start_message}"`);
1359
+ lines.push(`Say after completion: "${result.usage_disclosure.finish_message}"`);
1360
+ }
1361
+ if (result.post_use_policy_suggestion !== null && result.post_use_policy_suggestion !== undefined) {
1362
+ const suggestion = result.post_use_policy_suggestion;
1363
+ lines.push(`After completion: ${routeAfterUsePromptText(suggestion.question)}`);
1364
+ lines.push(`Policy command after confirmation: ${suggestion.suggested_policy.command_hint}`);
1365
+ }
1366
+ if (result.matched_capability === null) {
1367
+ lines.push("Possible skills:");
1368
+ for (const skill of result.possible_skills.slice(0, 5)) {
1369
+ lines.push(`- ${skill.id} (${skill.category ?? "uncategorized"}, allowed=${skill.allowed})`);
1370
+ }
1371
+ }
1372
+ lines.push("");
1373
+ return lines.join("\n");
1374
+ }
1375
+
1376
+ function routeCandidateStatus(candidate) {
1377
+ return [
1378
+ candidate.role,
1379
+ candidate.selected ? "selected" : null,
1380
+ candidate.guard_allowed ? "allowed" : "denied"
1381
+ ].filter((value) => value !== null).join(", ");
1382
+ }
1383
+
1384
+ function routeAfterUsePromptText(question) {
1385
+ return question.replace(/^Should I /u, "ask whether to ").replace(/\?$/u, ".");
1386
+ }
1387
+
1015
1388
  function renderSourceAudit(result) {
1016
1389
  const lines = [
1017
1390
  `Source audit: ${result.ok ? "passed" : "failed"}`,
@@ -1123,9 +1496,10 @@ function renderCounts(counts) {
1123
1496
 
1124
1497
  function helpText() {
1125
1498
  return [
1126
- "SkillBoard - workflow-scoped agent skill policy",
1499
+ "SkillBoard - AI-mediated workflow-scoped skill policy",
1500
+ "Version: see skillboard --version",
1127
1501
  "",
1128
- "Commands:",
1502
+ "AI/automation operations:",
1129
1503
  " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1130
1504
  " uninstall [--dir <path>] [--dry-run] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs]",
1131
1505
  " inventory refresh [--dir <path>] [--config <path>] [--scan-root <dir>[,<dir>]] [--dry-run] [--json]",
@@ -1133,7 +1507,7 @@ function helpText() {
1133
1507
  " sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]",
1134
1508
  " doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1135
1509
  " status [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1136
- " brief [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1510
+ " brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1137
1511
  " apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1138
1512
  " import --profile <id-or-path> --source-root <dir> [--profile-dirs a,b] [--out <path>]",
1139
1513
  " import --profile <id-or-path> --source-root <dir> --config <path> --merge [--replace] [--dry-run]",
@@ -1141,6 +1515,7 @@ function helpText() {
1141
1515
  " check --config <path> --skills <dir>",
1142
1516
  " list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]",
1143
1517
  " explain <skill-id> --config <path> --skills <dir> [--json]",
1518
+ " route <intent> --workflow <name> --config <path> --skills <dir> [--json]",
1144
1519
  " can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1145
1520
  " guard use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1146
1521
  " audit sources --config <path> --skills <dir> [--verify] [--json]",
@@ -1151,6 +1526,11 @@ function helpText() {
1151
1526
  " 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]",
1152
1527
  " add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]",
1153
1528
  " add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]",
1529
+ " 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]",
1530
+ " 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]",
1531
+ " variant status <variant-id> --config <path> --skills <dir> [--json]",
1532
+ " variant approve <variant-id> --config <path> --skills <dir> [--mode manual-only|router-only|workflow-auto] [--dry-run] [--json]",
1533
+ " variant reset <variant-id> --to-base|--to-approved --config <path> --skills <dir> [--yes] [--dry-run] [--mode manual-only|router-only|workflow-auto] [--json]",
1154
1534
  " activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]",
1155
1535
  " block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]",
1156
1536
  " quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]",
@@ -1160,12 +1540,187 @@ function helpText() {
1160
1540
  " reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]",
1161
1541
  " impact disable <skill-id> --config <path> --skills <dir> [--out <path>] [--json]",
1162
1542
  "",
1163
- "Approval loop:",
1164
- " Run skillboard brief --json --config <path> --skills <dir> [--workflow <name>] [--include-actions].",
1165
- " Pick one current action id from the brief and ask the user for confirmation.",
1166
- " Apply with skillboard apply-action <action-id> --config <path> --skills <dir> [--workflow <name>] --yes --json.",
1167
- " Read the returned post-apply brief, then run skillboard guard use before invocation.",
1543
+ "AI/automation control loop:",
1544
+ " Goal: preserve SkillBoard as a non-blocking AI skill routing control plane; see docs/ai-skill-routing-goal.md.",
1545
+ " Development loop: observe route work explain briefly → ask after remember policy.",
1546
+ " For an already-allowed skill, disclose the selected skill at start and completion; do not ask for another approval.",
1547
+ " Translate a user's skill request into the current brief: skillboard brief --json --config <path> --skills <dir> [--workflow <name>] [--intent <request>] [--include-actions].",
1548
+ " If a policy-changing action is needed, pick one current action id from that brief and ask the user for one confirmation.",
1549
+ " Apply one current action with skillboard apply-action <action-id> --config <path> --skills <dir> [--workflow <name>] --yes --json.",
1550
+ " Read the returned post-apply brief, then run skillboard guard use automatically before invocation.",
1168
1551
  " 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.",
1169
1552
  ""
1170
1553
  ].join("\n");
1171
1554
  }
1555
+
1556
+ function commandHelpText(command) {
1557
+ if (command === "brief") {
1558
+ return briefHelpText();
1559
+ }
1560
+ if (command === "init") {
1561
+ return initHelpText();
1562
+ }
1563
+ if (command === "doctor" || command === "status") {
1564
+ return doctorHelpText();
1565
+ }
1566
+ if (command === "route") {
1567
+ return routeHelpText();
1568
+ }
1569
+ if (command === "guard") {
1570
+ return guardHelpText();
1571
+ }
1572
+ if (command === "apply-action") {
1573
+ return applyActionHelpText();
1574
+ }
1575
+ const usage = COMMAND_USAGE.get(command);
1576
+ return usage === undefined ? null : genericCommandHelpText(usage);
1577
+ }
1578
+
1579
+ function genericCommandHelpText(usageLines) {
1580
+ return [
1581
+ ...usageLines.map((line) => `Usage: skillboard ${line}`),
1582
+ "",
1583
+ "This help is read-only. It does not load config or change project files.",
1584
+ "Run skillboard help for the full command catalog.",
1585
+ ""
1586
+ ].join("\n");
1587
+ }
1588
+
1589
+ function initHelpText() {
1590
+ return [
1591
+ "Usage: skillboard init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1592
+ "",
1593
+ "Creates the local SkillBoard control files for a project.",
1594
+ "Use it once per project before asking the AI which skills it can use.",
1595
+ "",
1596
+ "Options:",
1597
+ " --dir <path> Project root to initialize; defaults to the current directory.",
1598
+ " --scan-root <dir>[,<dir>] Add extra skill roots to scan during setup.",
1599
+ " --no-scan-installed Create the files without scanning installed agent skills.",
1600
+ "",
1601
+ "What changes:",
1602
+ " Writes skillboard.config.yaml, skills/, .skillboard/, AGENTS.md, and CLAUDE.md as needed.",
1603
+ " Imports trusted user-local skills as on-request skills.",
1604
+ " Keeps runtime, plugin, system, and external skills quarantined until reviewed.",
1605
+ "",
1606
+ "Next:",
1607
+ " Run skillboard doctor --summary.",
1608
+ " Ask your AI: \"What skills can you use in this project?\"",
1609
+ ""
1610
+ ].join("\n");
1611
+ }
1612
+
1613
+ function doctorHelpText() {
1614
+ return [
1615
+ "Usage: skillboard doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1616
+ "",
1617
+ "Checks whether a SkillBoard project is ready to use.",
1618
+ "The status command is an alias for doctor.",
1619
+ "",
1620
+ "Options:",
1621
+ " --dir <path> Project root to check; defaults to the current directory.",
1622
+ " --config <path> Use a specific skillboard.config.yaml.",
1623
+ " --skills <dir> Use a specific skills directory.",
1624
+ " --verify Verify local source/cache digests when available.",
1625
+ " --strict Return a failing exit code for review-needed safe mode.",
1626
+ " --summary Print a short human summary.",
1627
+ " --json Print an agent-readable payload.",
1628
+ "",
1629
+ "Use this after init and before trusting new skill sources.",
1630
+ ""
1631
+ ].join("\n");
1632
+ }
1633
+
1634
+ function briefHelpText() {
1635
+ return [
1636
+ "Usage: skillboard brief [--workflow <name>] [--intent <request>] [--dir <path>] [--config <path>] [--skills <dir>] [--include-actions] [--verbose] [--json]",
1637
+ "",
1638
+ "Reads the current SkillBoard brief without changing project files.",
1639
+ "Use it when a user asks what skills the AI can use, which skill fits a request, or what needs approval.",
1640
+ "",
1641
+ "Options:",
1642
+ " --workflow <name> Evaluate one workflow.",
1643
+ " --intent <request> Add a natural-language request so SkillBoard can suggest a skill.",
1644
+ " --dir <path> Use a project root; defaults to the current directory.",
1645
+ " --config <path> Use a specific skillboard.config.yaml.",
1646
+ " --skills <dir> Use a specific skills directory.",
1647
+ " --include-actions Include current action ids in JSON output.",
1648
+ " --verbose Show full lists instead of compact previews.",
1649
+ " --json Print an agent-readable payload.",
1650
+ "",
1651
+ "AI use:",
1652
+ " Read this before answering availability questions.",
1653
+ " Run skillboard guard use <skill-id> --workflow <name> before invoking a skill.",
1654
+ " If guard allows use, disclose the skill at the start and completion; do not ask for another approval.",
1655
+ " If a policy change is needed, ask the user to approve one current action id from this brief.",
1656
+ ""
1657
+ ].join("\n");
1658
+ }
1659
+
1660
+ function routeHelpText() {
1661
+ return [
1662
+ "Usage: skillboard route <intent> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
1663
+ "",
1664
+ "Suggests the best currently allowed skill for a user request.",
1665
+ "Use it when the AI needs a skill recommendation without changing policy.",
1666
+ "",
1667
+ "Options:",
1668
+ " <intent> Natural-language request, such as \"write tests first\".",
1669
+ " --workflow <name> Workflow to route within.",
1670
+ " --config <path> Use a specific skillboard.config.yaml.",
1671
+ " --skills <dir> Use a specific skills directory.",
1672
+ " --json Print an agent-readable payload.",
1673
+ "",
1674
+ "AI use:",
1675
+ " If a skill is recommended, run the guard before invoking it.",
1676
+ " If no skill matches, ask a clarifying question instead of guessing.",
1677
+ ""
1678
+ ].join("\n");
1679
+ }
1680
+
1681
+ function guardHelpText() {
1682
+ return [
1683
+ "Usage: skillboard guard use <skill-id> --workflow <name> [--config <path>] [--skills <dir>] [--json]",
1684
+ "",
1685
+ "Checks whether one skill may be used right now.",
1686
+ "Run this immediately before the AI invokes a skill.",
1687
+ "",
1688
+ "Options:",
1689
+ " use Guard a skill invocation.",
1690
+ " <skill-id> Skill id to check.",
1691
+ " --workflow <name> Workflow that will use the skill.",
1692
+ " --config <path> Use a specific skillboard.config.yaml.",
1693
+ " --skills <dir> Use a specific skills directory.",
1694
+ " --json Print an agent-readable payload.",
1695
+ "",
1696
+ "AI use:",
1697
+ " If allowed, disclose the skill at the start and completion.",
1698
+ " If denied, do not invoke the skill. Explain the reason or ask for the needed policy change.",
1699
+ ""
1700
+ ].join("\n");
1701
+ }
1702
+
1703
+ function applyActionHelpText() {
1704
+ return [
1705
+ "Usage: skillboard apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1706
+ "",
1707
+ "Applies one current action id from the latest brief.",
1708
+ "Use it only after the user confirms a policy-changing action.",
1709
+ "",
1710
+ "Options:",
1711
+ " <action-id> One action id from the current brief.",
1712
+ " --workflow <name> Workflow for workflow-scoped actions.",
1713
+ " --dir <path> Project root; defaults to the current directory.",
1714
+ " --config <path> Use a specific skillboard.config.yaml.",
1715
+ " --skills <dir> Use a specific skills directory.",
1716
+ " --dry-run Preview the action without writing changes.",
1717
+ " --yes Apply the action after user confirmation.",
1718
+ " --allow-destructive Permit destructive reset or cleanup actions.",
1719
+ " --json Print an agent-readable payload.",
1720
+ "",
1721
+ "AI use:",
1722
+ " Apply exactly one current action id.",
1723
+ " Read the returned post-apply brief before making another availability decision.",
1724
+ ""
1725
+ ].join("\n");
1726
+ }