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