agent-skillboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CONTRIBUTING.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +645 -0
  4. package/bin/skillboard.mjs +4 -0
  5. package/docs/adapters.md +127 -0
  6. package/docs/capabilities.md +107 -0
  7. package/docs/install.md +346 -0
  8. package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
  9. package/docs/plans/README.md +20 -0
  10. package/docs/policy-model.md +251 -0
  11. package/docs/positioning.md +94 -0
  12. package/docs/profiles.md +166 -0
  13. package/docs/rollout-runbook.md +60 -0
  14. package/docs/user-flow.md +231 -0
  15. package/docs/versioning.md +201 -0
  16. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
  17. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
  18. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
  19. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
  20. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
  21. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
  22. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
  23. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
  24. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
  25. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
  26. package/examples/multi-source.config.yaml +271 -0
  27. package/examples/skillboard.config.yaml +194 -0
  28. package/examples/skills/grill-me/SKILL.md +9 -0
  29. package/examples/skills/grill-with-docs/SKILL.md +9 -0
  30. package/examples/skills/requirement-intake/SKILL.md +9 -0
  31. package/examples/skills/tdd/SKILL.md +8 -0
  32. package/package.json +58 -0
  33. package/profiles/anthropics-skills.yaml +17 -0
  34. package/profiles/mattpocock-skills.yaml +26 -0
  35. package/profiles/oh-my-openagent.yaml +32 -0
  36. package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
  37. package/profiles/wshobson-agents.yaml +19 -0
  38. package/src/advisor/action-core.mjs +74 -0
  39. package/src/advisor/actions.mjs +256 -0
  40. package/src/advisor/application-commands.mjs +59 -0
  41. package/src/advisor/apply-action.mjs +183 -0
  42. package/src/advisor/schema.mjs +112 -0
  43. package/src/advisor/setup-actions.mjs +42 -0
  44. package/src/advisor/skills.mjs +191 -0
  45. package/src/advisor/sort.mjs +15 -0
  46. package/src/advisor/sources.mjs +160 -0
  47. package/src/advisor/trust-policy.mjs +65 -0
  48. package/src/advisor/workflow.mjs +41 -0
  49. package/src/advisor.mjs +134 -0
  50. package/src/agent-inventory-platforms.mjs +100 -0
  51. package/src/agent-inventory.mjs +804 -0
  52. package/src/brief-cli.mjs +40 -0
  53. package/src/brief-renderer.mjs +362 -0
  54. package/src/change-plan.mjs +169 -0
  55. package/src/cli.mjs +1171 -0
  56. package/src/config-helpers.mjs +72 -0
  57. package/src/control/can-use-guard.mjs +91 -0
  58. package/src/control/config-write.mjs +163 -0
  59. package/src/control/skill-crud.mjs +394 -0
  60. package/src/control/source-trust.mjs +161 -0
  61. package/src/control/workflow-crud.mjs +104 -0
  62. package/src/control.mjs +197 -0
  63. package/src/doctor.mjs +299 -0
  64. package/src/domain/constants.mjs +47 -0
  65. package/src/domain/indexes.mjs +29 -0
  66. package/src/domain/rules/capabilities.mjs +33 -0
  67. package/src/domain/rules/harnesses.mjs +16 -0
  68. package/src/domain/rules/install-units.mjs +95 -0
  69. package/src/domain/rules/skills.mjs +105 -0
  70. package/src/domain/rules/workflows.mjs +105 -0
  71. package/src/domain/skill-state-matrix.mjs +79 -0
  72. package/src/domain/source-classes.mjs +99 -0
  73. package/src/hook-plan.mjs +152 -0
  74. package/src/impact.mjs +41 -0
  75. package/src/index.mjs +82 -0
  76. package/src/init.mjs +136 -0
  77. package/src/install-output-detector.mjs +337 -0
  78. package/src/install-units.mjs +62 -0
  79. package/src/inventory-refresh.mjs +45 -0
  80. package/src/lifecycle-cli.mjs +166 -0
  81. package/src/lifecycle-content.mjs +87 -0
  82. package/src/policy.mjs +42 -0
  83. package/src/reconcile.mjs +111 -0
  84. package/src/report.mjs +151 -0
  85. package/src/review.mjs +88 -0
  86. package/src/rollout.mjs +453 -0
  87. package/src/skill-paths.mjs +17 -0
  88. package/src/source-cache.mjs +178 -0
  89. package/src/source-profile-loader.mjs +160 -0
  90. package/src/source-profiles.mjs +299 -0
  91. package/src/source-verification.mjs +252 -0
  92. package/src/uninstall.mjs +258 -0
  93. package/src/workspace.mjs +219 -0
  94. package/tsconfig.lsp.json +15 -0
package/src/cli.mjs ADDED
@@ -0,0 +1,1171 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute } from "node:path";
3
+ import YAML from "yaml";
4
+ import {
5
+ activateSkill,
6
+ addHarness,
7
+ addSkill,
8
+ addWorkflow,
9
+ auditSources,
10
+ blockSkill,
11
+ canUseSkill,
12
+ checkPolicy,
13
+ detectInstallOutput,
14
+ doctorProject,
15
+ explainSkill,
16
+ importSource,
17
+ installGuardHook,
18
+ impactDisable,
19
+ listHarnesses,
20
+ listInstallUnits,
21
+ listSkills,
22
+ listWorkflows,
23
+ loadSourceProfile,
24
+ loadWorkspace,
25
+ mergeImportFragment,
26
+ preferSkill,
27
+ quarantineSkill,
28
+ removeSkill,
29
+ reconcileWorkspace,
30
+ refreshAgentInventory,
31
+ refreshSourcePins,
32
+ renderDashboard,
33
+ renderImportFragment,
34
+ renderReconcilePlan,
35
+ reviewInstallUnit,
36
+ rolloutApply,
37
+ rolloutAudit,
38
+ rolloutPlan,
39
+ rolloutReport,
40
+ rolloutRollback,
41
+ verifySources,
42
+ writeLockfile
43
+ } from "./index.mjs";
44
+ // 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.
45
+ import { ApplyActionError, applyActionErrorPayload, applyAdvisorAction } from "./advisor/apply-action.mjs";
46
+ import { runBriefCommand } from "./brief-cli.mjs";
47
+ import { renderSkillBrief } from "./brief-renderer.mjs";
48
+ import { planGuardHookInstall } from "./control.mjs";
49
+ import { writeCheckedConfig } from "./control/config-write.mjs";
50
+ import { runInitCommand, runUninstallCommand } from "./lifecycle-cli.mjs";
51
+
52
+ const APPLY_ACTION_VALUE_OPTIONS = new Set(["workflow", "dir", "config", "skills", "out", "skillboard-bin"]);
53
+
54
+ export async function main(argv, stdout, stderr) {
55
+ try {
56
+ return await run(argv, stdout, stderr);
57
+ } catch (error) {
58
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
59
+ return 1;
60
+ }
61
+ }
62
+
63
+ async function run(argv, stdout, stderr) {
64
+ const command = argv[0] ?? "help";
65
+ const options = parseOptions(argv.slice(1));
66
+ switch (command) {
67
+ case "init":
68
+ return await runInitCommand(options, stdout);
69
+ case "uninstall":
70
+ return await runUninstallCommand(options, stdout);
71
+ case "inventory":
72
+ return await inventory(argv.slice(1), options, stdout);
73
+ case "sources":
74
+ return await sources(argv.slice(1), options, stdout);
75
+ case "import":
76
+ return await importProfile(options, stdout);
77
+ case "scan":
78
+ return await scan(options, stdout);
79
+ case "check":
80
+ return await check(options, stdout, stderr);
81
+ case "doctor":
82
+ case "status":
83
+ return await doctor(options, stdout);
84
+ case "brief":
85
+ return await runBriefCommand(options, stdout, {
86
+ configPath: configPath(options),
87
+ skillsRoot: skillsRoot(options)
88
+ });
89
+ case "apply-action":
90
+ return await applyActionCommand(argv.slice(1), options, stdout, stderr);
91
+ case "list":
92
+ return await list(argv.slice(1), options, stdout);
93
+ case "explain":
94
+ return await explain(argv.slice(1), options, stdout);
95
+ case "can-use":
96
+ return await canUse(argv.slice(1), options, stdout);
97
+ case "guard":
98
+ return await guard(argv.slice(1), options, stdout);
99
+ case "audit":
100
+ return await audit(argv.slice(1), options, stdout);
101
+ case "hook":
102
+ return await hook(argv.slice(1), options, stdout);
103
+ case "lock":
104
+ return await lock(argv.slice(1), options, stdout);
105
+ case "review":
106
+ return await review(argv.slice(1), options, stdout);
107
+ case "rollout":
108
+ return await rollout(argv.slice(1), options, stdout);
109
+ case "activate":
110
+ return await activate(argv.slice(1), options, stdout);
111
+ case "add":
112
+ return await add(argv.slice(1), options, stdout);
113
+ case "block":
114
+ return await block(argv.slice(1), options, stdout);
115
+ case "quarantine":
116
+ return await quarantine(argv.slice(1), options, stdout);
117
+ case "prefer":
118
+ return await prefer(argv.slice(1), options, stdout);
119
+ case "remove":
120
+ return await remove(argv.slice(1), options, stdout);
121
+ case "dashboard":
122
+ return await dashboard(options, stdout);
123
+ case "reconcile":
124
+ return await reconcile(options, stdout);
125
+ case "impact":
126
+ 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
+ }
138
+
139
+ async function importProfile(options, stdout) {
140
+ const profileRef = options.get("profile");
141
+ const sourceRoot = options.get("source-root");
142
+ if (profileRef === undefined || sourceRoot === undefined) {
143
+ throw new Error("Usage: skillboard import --profile <id-or-path> --source-root <dir>");
144
+ }
145
+ const profile = await loadSourceProfile(profileRef, { profileDirs: readCsv(options.get("profile-dirs")) });
146
+ const imported = await importSource({ profile, sourceRoot });
147
+ if (options.get("merge") === "true") {
148
+ return await mergeImport(options, imported, stdout);
149
+ }
150
+ const fragment = renderImportFragment(imported);
151
+ const out = options.get("out");
152
+ if (out === undefined) {
153
+ stdout.write(fragment);
154
+ return 0;
155
+ }
156
+ await mkdir(dirname(out), { recursive: true });
157
+ await writeFile(out, fragment, "utf8");
158
+ stdout.write(`Import fragment written: ${out}\n`);
159
+ return 0;
160
+ }
161
+
162
+ async function mergeImport(options, imported, stdout) {
163
+ const path = configPath(options);
164
+ const originalText = await readFile(path, "utf8");
165
+ const merged = mergeImportFragment(originalText, imported, { replace: options.get("replace") === "true" });
166
+ const document = YAML.parseDocument(merged.text);
167
+ if (document.errors.length > 0) {
168
+ throw new Error(`Invalid YAML config after import merge: ${document.errors.map((error) => error.message).join("; ")}`);
169
+ }
170
+ const checked = await writeCheckedConfig(document, originalText, {
171
+ configPath: path,
172
+ skillsRoot: skillsRoot(options),
173
+ dryRun: options.get("dry-run") === "true"
174
+ }, `Import merged: ${path}`);
175
+ const result = {
176
+ ...checked,
177
+ addedSkills: merged.addedSkills,
178
+ addedInstallUnits: merged.addedInstallUnits,
179
+ replacedSkills: merged.replacedSkills,
180
+ replacedInstallUnits: merged.replacedInstallUnits
181
+ };
182
+ writeOutput(stdout, result, options, () => renderImportMerge(result));
183
+ return 0;
184
+ }
185
+
186
+ async function inventory(argv, options, stdout) {
187
+ const args = positionalArgs(argv);
188
+ if (args[0] === "detect") {
189
+ return await inventoryDetect(options, stdout);
190
+ }
191
+ if (args[0] !== "refresh") {
192
+ throw new Error("Usage: skillboard inventory refresh|detect ...");
193
+ }
194
+ const result = await refreshAgentInventory({
195
+ root: options.get("dir") ?? ".",
196
+ configPath: options.get("config"),
197
+ roots: readCsv(options.get("scan-root")),
198
+ dryRun: options.get("dry-run") === "true"
199
+ });
200
+ writeOutput(stdout, result, options, () => renderInventoryRefresh(result));
201
+ return 0;
202
+ }
203
+
204
+ async function inventoryDetect(options, stdout) {
205
+ const result = await detectInstallOutput({
206
+ root: options.get("dir") ?? ".",
207
+ configPath: options.get("config"),
208
+ unitId: options.get("unit"),
209
+ installOutputPath: options.get("install-output"),
210
+ configFiles: readCsv(options.get("config-file")),
211
+ kind: options.get("kind"),
212
+ source: options.get("source"),
213
+ scope: options.get("scope"),
214
+ dryRun: options.get("dry-run") === "true"
215
+ });
216
+ writeOutput(stdout, result, options, () => renderInstallDetection(result));
217
+ return 0;
218
+ }
219
+
220
+ async function sources(argv, options, stdout) {
221
+ const args = positionalArgs(argv);
222
+ if (args[0] !== "refresh") {
223
+ throw new Error("Usage: skillboard sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]");
224
+ }
225
+ const result = await refreshSourcePins({
226
+ root: options.get("dir") ?? ".",
227
+ configPath: options.get("config"),
228
+ cacheDir: options.get("cache-dir"),
229
+ units: readCsv(options.get("unit")),
230
+ dryRun: options.get("dry-run") === "true"
231
+ });
232
+ writeOutput(stdout, result, options, () => renderSourceRefresh(result));
233
+ return 0;
234
+ }
235
+
236
+ async function scan(options, stdout) {
237
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
238
+ stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
239
+ return 0;
240
+ }
241
+
242
+ async function check(options, stdout, stderr) {
243
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
244
+ const result = checkPolicy(workspace);
245
+ const output = [...result.errors, ...result.warnings].join("\n");
246
+ if (output.length > 0) {
247
+ (result.ok ? stdout : stderr).write(`${output}\n`);
248
+ }
249
+ stdout.write(result.ok ? "Policy check passed\n" : "Policy check failed\n");
250
+ return result.ok ? 0 : 1;
251
+ }
252
+
253
+ async function doctor(options, stdout) {
254
+ const result = await doctorProject({
255
+ root: options.get("dir") ?? ".",
256
+ configPath: options.get("config"),
257
+ skillsRoot: options.get("skills"),
258
+ verifySources: options.get("verify") === "true"
259
+ });
260
+ const summary = options.get("summary") === "true";
261
+ writeOutput(stdout, result, options, () => summary ? renderDoctorSummary(result) : renderDoctor(result));
262
+ return (options.get("strict") === "true" ? result.strictOk : result.ok) ? 0 : 1;
263
+ }
264
+
265
+ async function applyActionCommand(argv, options, stdout, stderr) {
266
+ try {
267
+ const actionIds = positionalArgs(argv, APPLY_ACTION_VALUE_OPTIONS);
268
+ if (actionIds.length !== 1) {
269
+ throw new ApplyActionError(
270
+ actionIds.length === 0 ? "missing-action-id" : "multiple-action-ids",
271
+ "Usage: skillboard apply-action <action-id>; apply exactly one action at a time."
272
+ );
273
+ }
274
+ const [actionId] = actionIds;
275
+ const result = await applyAdvisorAction(actionId, {
276
+ root: commandRoot(options),
277
+ workflow: options.get("workflow"),
278
+ configPath: configPath(options),
279
+ skillsRoot: skillsRoot(options),
280
+ dryRun: options.get("dry-run") === "true",
281
+ yes: options.get("yes") === "true",
282
+ allowDestructive: options.get("allow-destructive") === "true",
283
+ hookOut: options.get("out"),
284
+ skillboardBin: options.get("skillboard-bin")
285
+ });
286
+ writeOutput(stdout, result, options, () => renderApplyAction(result));
287
+ return 0;
288
+ } catch (error) {
289
+ const payload = applyActionErrorPayload(error);
290
+ if (jsonRequested(options, argv)) {
291
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
292
+ } else {
293
+ stderr.write(`${payload.error.message}\n`);
294
+ }
295
+ return 1;
296
+ }
297
+ }
298
+
299
+ async function list(argv, options, stdout) {
300
+ const kind = positionalArgs(argv)[0] ?? "skills";
301
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
302
+ let result;
303
+ if (kind === "skills") {
304
+ result = listSkills(workspace, { workflow: options.get("workflow") });
305
+ } else if (kind === "workflows") {
306
+ result = listWorkflows(workspace);
307
+ } else if (kind === "harnesses") {
308
+ result = listHarnesses(workspace);
309
+ } else if (kind === "install-units") {
310
+ result = listInstallUnits(workspace);
311
+ } else {
312
+ throw new Error("Usage: skillboard list [skills|workflows|harnesses|install-units]");
313
+ }
314
+ writeOutput(stdout, result, options, () => renderList(kind, result));
315
+ return 0;
316
+ }
317
+
318
+ async function explain(argv, options, stdout) {
319
+ const skillId = positionalArgs(argv)[0];
320
+ if (skillId === undefined) {
321
+ throw new Error("Usage: skillboard explain <skill-id>");
322
+ }
323
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
324
+ const result = explainSkill(workspace, skillId);
325
+ writeOutput(stdout, result, options, () => renderExplain(result));
326
+ return 0;
327
+ }
328
+
329
+ async function canUse(argv, options, stdout) {
330
+ const skillId = positionalArgs(argv)[0];
331
+ const workflow = options.get("workflow");
332
+ if (skillId === undefined || workflow === undefined) {
333
+ throw new Error("Usage: skillboard can-use <skill-id> --workflow <name>");
334
+ }
335
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
336
+ const result = canUseSkill(workspace, skillId, workflow);
337
+ writeOutput(stdout, result, options, () => renderCanUse(result));
338
+ return result.allowed ? 0 : 2;
339
+ }
340
+
341
+ async function guard(argv, options, stdout) {
342
+ const args = positionalArgs(argv);
343
+ const skillId = args[0] === "use" ? args[1] : args[0];
344
+ const workflow = options.get("workflow");
345
+ if (skillId === undefined || workflow === undefined) {
346
+ throw new Error("Usage: skillboard guard use <skill-id> --workflow <name>");
347
+ }
348
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
349
+ const result = canUseSkill(workspace, skillId, workflow);
350
+ if (options.get("json") === "true") {
351
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
352
+ } else {
353
+ stdout.write(result.allowed ? "allow\n" : `deny\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}\n`);
354
+ }
355
+ return result.allowed ? 0 : 2;
356
+ }
357
+
358
+ async function audit(argv, options, stdout) {
359
+ const args = positionalArgs(argv);
360
+ if ((args[0] ?? "sources") !== "sources") {
361
+ throw new Error("Usage: skillboard audit sources --config <path> --skills <dir> [--json]");
362
+ }
363
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
364
+ const result = options.get("verify") === "true"
365
+ ? await verifySources(workspace, { configPath: configPath(options) })
366
+ : auditSources(workspace);
367
+ writeOutput(stdout, result, options, () => renderSourceAudit(result));
368
+ return result.ok ? 0 : 1;
369
+ }
370
+
371
+ async function rollout(argv, options, stdout) {
372
+ const action = positionalArgs(argv)[0] ?? "audit";
373
+ const rolloutOptions = {
374
+ root: options.get("dir") ?? ".",
375
+ configPath: options.get("config"),
376
+ skillsRoot: options.get("skills"),
377
+ rolloutsDir: options.get("rollouts-dir"),
378
+ transaction: options.get("transaction")
379
+ };
380
+ let result;
381
+ if (action === "audit") {
382
+ result = await rolloutAudit(rolloutOptions);
383
+ } else if (action === "plan") {
384
+ result = await rolloutPlan(rolloutOptions);
385
+ } else if (action === "apply") {
386
+ result = await rolloutApply(rolloutOptions);
387
+ } else if (action === "rollback") {
388
+ result = await rolloutRollback(rolloutOptions);
389
+ } else if (action === "report") {
390
+ result = await rolloutReport(rolloutOptions);
391
+ } else {
392
+ throw new Error("Usage: skillboard rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--json]");
393
+ }
394
+ writeOutput(stdout, result, options, () => renderRollout(result));
395
+ return result.exitCode;
396
+ }
397
+
398
+ async function hook(argv, options, stdout) {
399
+ const args = positionalArgs(argv);
400
+ if (args[0] !== "install") {
401
+ throw new Error("Usage: skillboard hook install --workflow <name> [--out <path>] [--skillboard-bin <path>] [--dry-run]");
402
+ }
403
+ const workflow = options.get("workflow");
404
+ if (workflow === undefined) {
405
+ throw new Error("Usage: skillboard hook install --workflow <name> [--out <path>] [--skillboard-bin <path>] [--dry-run]");
406
+ }
407
+ const hookOptions = {
408
+ workflow,
409
+ out: options.get("out"),
410
+ command: options.get("skillboard-bin"),
411
+ configPath: configPath(options),
412
+ skillsRoot: skillsRoot(options)
413
+ };
414
+ if (options.get("dry-run") === "true") {
415
+ return await dryRunHookInstall(hookOptions, options, stdout);
416
+ }
417
+ return await applyHookInstall(hookOptions, options, stdout);
418
+ }
419
+
420
+ async function dryRunHookInstall(hookOptions, options, stdout) {
421
+ let planned;
422
+ try {
423
+ planned = await planGuardHookInstall(hookOptions);
424
+ } catch (error) {
425
+ return writeHookInstallError(stdout, options, hookInstallError(error));
426
+ }
427
+ if (planned.target_exists) {
428
+ return writeHookInstallError(stdout, options, {
429
+ code: "hook_path_exists",
430
+ message: `Refusing to overwrite existing hook path: ${planned.path}`
431
+ }, planned);
432
+ }
433
+ const result = { ok: true, planned };
434
+ writeOutput(stdout, result, options, () => renderHookInstallDryRun(result));
435
+ return 0;
436
+ }
437
+
438
+ async function applyHookInstall(hookOptions, options, stdout) {
439
+ let planned;
440
+ try {
441
+ planned = await planGuardHookInstall(hookOptions);
442
+ if (planned.target_exists) {
443
+ return writeHookInstallError(stdout, options, {
444
+ code: "hook_path_exists",
445
+ message: `Refusing to overwrite existing hook path: ${planned.path}`
446
+ }, planned);
447
+ }
448
+ const result = await installGuardHook(hookOptions);
449
+ writeOutput(stdout, result, options, () => `Installed guard hook: ${result.path}\n`);
450
+ return 0;
451
+ } catch (error) {
452
+ return writeHookInstallError(stdout, options, hookInstallError(error), planned);
453
+ }
454
+ }
455
+
456
+ async function lock(argv, options, stdout) {
457
+ const args = positionalArgs(argv);
458
+ if (args[0] !== "write") {
459
+ throw new Error("Usage: skillboard lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]");
460
+ }
461
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
462
+ const out = options.get("out") ?? "skillboard.lock.yaml";
463
+ const result = await writeLockfile(workspace, {
464
+ configPath: configPath(options),
465
+ skillsRoot: skillsRoot(options),
466
+ out,
467
+ replace: options.get("replace") === "true",
468
+ allowUnverified: options.get("allow-unverified") === "true"
469
+ });
470
+ writeOutput(stdout, result, options, () => `Lockfile written: ${result.path}\n`);
471
+ return 0;
472
+ }
473
+
474
+ async function review(argv, options, stdout) {
475
+ const args = positionalArgs(argv);
476
+ if (args[0] !== "install-unit" || args[1] === undefined) {
477
+ throw new Error("Usage: skillboard review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]");
478
+ }
479
+ const result = await reviewInstallUnit({
480
+ unitId: args[1],
481
+ trustLevel: options.get("trust-level"),
482
+ configPath: configPath(options),
483
+ skillsRoot: skillsRoot(options),
484
+ dryRun: options.get("dry-run") === "true"
485
+ });
486
+ writeControlResult(stdout, result, options);
487
+ return 0;
488
+ }
489
+
490
+ async function activate(argv, options, stdout) {
491
+ const skillId = positionalArgs(argv)[0];
492
+ const workflow = options.get("workflow");
493
+ if (skillId === undefined || workflow === undefined) {
494
+ throw new Error("Usage: skillboard activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto]");
495
+ }
496
+ const result = await activateSkill({
497
+ skillId,
498
+ workflow,
499
+ mode: options.get("mode"),
500
+ configPath: configPath(options),
501
+ skillsRoot: skillsRoot(options),
502
+ dryRun: options.get("dry-run") === "true"
503
+ });
504
+ writeControlResult(stdout, result, options);
505
+ return 0;
506
+ }
507
+
508
+ async function add(argv, options, stdout) {
509
+ const args = positionalArgs(argv);
510
+ if (args[0] === "workflow") {
511
+ return await addWorkflowCommand(args, options, stdout);
512
+ }
513
+ if (args[0] === "harness") {
514
+ return await addHarnessCommand(args, options, stdout);
515
+ }
516
+ if (args[0] !== "skill" || args[1] === undefined || options.get("path") === undefined) {
517
+ throw new Error("Usage: skillboard add skill <skill-id> --path <relative-skill-path> --config <path> --skills <dir> [--workflow <name>] [--dry-run] [--json]");
518
+ }
519
+ const result = await addSkill({
520
+ skillId: args[1],
521
+ path: options.get("path"),
522
+ status: options.get("status"),
523
+ invocation: options.get("invocation"),
524
+ exposure: options.get("exposure"),
525
+ category: options.get("category"),
526
+ ownerInstallUnit: options.get("owner-install-unit"),
527
+ workflow: options.get("workflow"),
528
+ configPath: configPath(options),
529
+ skillsRoot: skillsRoot(options),
530
+ dryRun: options.get("dry-run") === "true"
531
+ });
532
+ writeControlResult(stdout, result, options);
533
+ return 0;
534
+ }
535
+
536
+ async function addWorkflowCommand(args, options, stdout) {
537
+ const workflow = args[1];
538
+ let harness = options.get("harness");
539
+ if (workflow === undefined || harness === undefined) {
540
+ if (workflow !== undefined && harness === undefined) {
541
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
542
+ const available = workspace.harnesses.map((h) => h.name).sort();
543
+ const hint = available.length === 0
544
+ ? "No harnesses are configured yet. Add one with: skillboard add harness <name>"
545
+ : `Available harnesses: ${available.join(", ")}`;
546
+ throw new Error(`--harness is required for workflow "${workflow}".\n${hint}\n\nUsage: skillboard add workflow <workflow-name> --harness <harness-name> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]`);
547
+ }
548
+ throw new Error("Usage: skillboard add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]");
549
+ }
550
+ const result = await addWorkflow({
551
+ workflow,
552
+ harness,
553
+ skills: readCsv(options.get("skill")),
554
+ harnessStatus: options.get("harness-status"),
555
+ requireExistingHarness: options.get("require-existing-harness") === "true",
556
+ configPath: configPath(options),
557
+ skillsRoot: skillsRoot(options),
558
+ dryRun: options.get("dry-run") === "true"
559
+ });
560
+ writeControlResult(stdout, result, options);
561
+ return 0;
562
+ }
563
+
564
+ async function addHarnessCommand(args, options, stdout) {
565
+ const harness = args[1];
566
+ if (harness === undefined) {
567
+ throw new Error("Usage: skillboard add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]");
568
+ }
569
+ const result = await addHarness({
570
+ harness,
571
+ status: options.get("status"),
572
+ commands: readCsv(options.get("command")),
573
+ configPath: configPath(options),
574
+ skillsRoot: skillsRoot(options),
575
+ dryRun: options.get("dry-run") === "true"
576
+ });
577
+ writeControlResult(stdout, result, options);
578
+ return 0;
579
+ }
580
+
581
+ async function block(argv, options, stdout) {
582
+ const skillId = positionalArgs(argv)[0];
583
+ const workflow = options.get("workflow");
584
+ if (skillId === undefined || workflow === undefined) {
585
+ throw new Error("Usage: skillboard block <skill-id> --workflow <name>");
586
+ }
587
+ const result = await blockSkill({
588
+ skillId,
589
+ workflow,
590
+ configPath: configPath(options),
591
+ skillsRoot: skillsRoot(options),
592
+ dryRun: options.get("dry-run") === "true"
593
+ });
594
+ writeControlResult(stdout, result, options);
595
+ return 0;
596
+ }
597
+
598
+ async function quarantine(argv, options, stdout) {
599
+ const skillId = positionalArgs(argv)[0];
600
+ if (skillId === undefined) {
601
+ throw new Error("Usage: skillboard quarantine <skill-id>");
602
+ }
603
+ const result = await quarantineSkill({
604
+ skillId,
605
+ configPath: configPath(options),
606
+ skillsRoot: skillsRoot(options),
607
+ dryRun: options.get("dry-run") === "true"
608
+ });
609
+ writeControlResult(stdout, result, options);
610
+ return 0;
611
+ }
612
+
613
+ async function prefer(argv, options, stdout) {
614
+ const skillId = positionalArgs(argv)[0];
615
+ const workflow = options.get("workflow");
616
+ const capability = options.get("capability");
617
+ if (skillId === undefined || workflow === undefined || capability === undefined) {
618
+ throw new Error("Usage: skillboard prefer <skill-id> --workflow <name> --capability <name>");
619
+ }
620
+ const result = await preferSkill({
621
+ skillId,
622
+ workflow,
623
+ capability,
624
+ configPath: configPath(options),
625
+ skillsRoot: skillsRoot(options),
626
+ dryRun: options.get("dry-run") === "true"
627
+ });
628
+ writeControlResult(stdout, result, options);
629
+ return 0;
630
+ }
631
+
632
+ async function remove(argv, options, stdout) {
633
+ const args = positionalArgs(argv);
634
+ if (args[0] !== "skill" || args[1] === undefined) {
635
+ throw new Error("Usage: skillboard remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]");
636
+ }
637
+ const result = await removeSkill({
638
+ skillId: args[1],
639
+ force: options.get("force") === "true",
640
+ configPath: configPath(options),
641
+ skillsRoot: skillsRoot(options),
642
+ dryRun: options.get("dry-run") === "true"
643
+ });
644
+ writeControlResult(stdout, result, options);
645
+ return 0;
646
+ }
647
+
648
+ async function dashboard(options, stdout) {
649
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
650
+ const markdown = renderDashboard(workspace);
651
+ const out = options.get("out");
652
+ if (out === undefined) {
653
+ stdout.write(markdown);
654
+ return 0;
655
+ }
656
+ await mkdir(dirname(out), { recursive: true });
657
+ await writeFile(out, markdown, "utf8");
658
+ stdout.write(`Dashboard written: ${out}\n`);
659
+ return 0;
660
+ }
661
+
662
+ async function reconcile(options, stdout) {
663
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
664
+ const plan = reconcileWorkspace(workspace, { actualHarnesses: readCsv(options.get("actual-harnesses")) });
665
+ const markdown = renderReconcilePlan(plan);
666
+ const out = options.get("out");
667
+ if (out === undefined) {
668
+ stdout.write(markdown);
669
+ return 0;
670
+ }
671
+ await mkdir(dirname(out), { recursive: true });
672
+ await writeFile(out, markdown, "utf8");
673
+ stdout.write(`Reconcile plan written: ${out}\n`);
674
+ return 0;
675
+ }
676
+
677
+ async function impact(argv, options, stdout) {
678
+ if (argv[0] !== "disable" || argv[1] === undefined) {
679
+ throw new Error("Usage: skillboard impact disable <skill-id>");
680
+ }
681
+ const workspace = await loadWorkspace({ configPath: configPath(options), skillsRoot: skillsRoot(options) });
682
+ const result = impactDisable(workspace, argv[1]);
683
+ if (options.get("json") === "true") {
684
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
685
+ return 0;
686
+ }
687
+ const markdown = renderImpact(result);
688
+ const out = options.get("out");
689
+ if (out === undefined) {
690
+ stdout.write(markdown);
691
+ return 0;
692
+ }
693
+ await mkdir(dirname(out), { recursive: true });
694
+ await writeFile(out, markdown, "utf8");
695
+ stdout.write(`Impact report written: ${out}\n`);
696
+ return 0;
697
+ }
698
+
699
+ function renderImpact(result) {
700
+ return [
701
+ `# Disable Impact: ${result.skillId}`,
702
+ "",
703
+ `- Found: \`${result.exists}\``,
704
+ `- 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
+
712
+ function configPath(options) {
713
+ return options.get("config") ?? "skillboard.config.yaml";
714
+ }
715
+
716
+ function skillsRoot(options) {
717
+ return options.get("skills") ?? "skills";
718
+ }
719
+
720
+ function commandRoot(options) {
721
+ const dir = options.get("dir");
722
+ if (dir !== undefined) {
723
+ return dir;
724
+ }
725
+ const config = options.get("config");
726
+ return config !== undefined && isAbsolute(config) ? dirname(config) : ".";
727
+ }
728
+
729
+ function parseOptions(args) {
730
+ const options = new Map();
731
+ for (let index = 0; index < args.length; index += 1) {
732
+ const arg = args[index];
733
+ if (!arg.startsWith("--")) {
734
+ continue;
735
+ }
736
+ const key = arg.slice(2);
737
+ const value = args[index + 1];
738
+ if (value === undefined || value.startsWith("--")) {
739
+ options.set(key, "true");
740
+ continue;
741
+ }
742
+ options.set(key, value);
743
+ index += 1;
744
+ }
745
+ return options;
746
+ }
747
+
748
+ function formatList(values) {
749
+ return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
750
+ }
751
+
752
+ function readCsv(value) {
753
+ if (value === undefined || value.trim() === "") {
754
+ return [];
755
+ }
756
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
757
+ }
758
+
759
+ function renderImportMerge(result) {
760
+ const lines = [
761
+ `${result.dryRun ? "Dry run: " : ""}${result.message}`,
762
+ renderChangePlan(result.plan).trimEnd(),
763
+ `Skills: ${formatList(result.addedSkills)}`,
764
+ `Install units: ${formatList(result.addedInstallUnits)}`
765
+ ];
766
+ if (result.replacedSkills.length > 0 || result.replacedInstallUnits.length > 0) {
767
+ lines.push(`Replaced skills: ${formatList(result.replacedSkills)}`);
768
+ lines.push(`Replaced install units: ${formatList(result.replacedInstallUnits)}`);
769
+ }
770
+ lines.push("");
771
+ return lines.join("\n");
772
+ }
773
+
774
+ function renderInventoryRefresh(result) {
775
+ return [
776
+ `${result.dryRun ? "Dry run: " : ""}Inventory refreshed: ${result.configPath}`,
777
+ renderChangePlan(result.plan).trimEnd(),
778
+ `Scanned skills: ${result.scan.scannedSkills}`,
779
+ `Scanned install units: ${result.scan.scannedInstallUnits}`,
780
+ `Added skills: ${formatList(result.scan.addedSkills)}`,
781
+ `Added install units: ${formatList(result.scan.addedInstallUnits)}`,
782
+ `Updated install units: ${formatList(result.scan.updatedInstallUnits)}`,
783
+ `Added workflows: ${formatList(result.scan.addedWorkflows ?? [])}`,
784
+ `Added harnesses: ${formatList(result.scan.addedHarnesses ?? [])}`,
785
+ `Skipped existing skills: ${formatList(result.scan.skippedSkills)}`,
786
+ `Review notes: ${formatList(result.scan.reviewNotes ?? [])}`,
787
+ `Scan warnings: ${formatList(result.scan.warnings ?? [])}`,
788
+ ""
789
+ ].join("\n");
790
+ }
791
+
792
+ function renderInstallDetection(result) {
793
+ return [
794
+ `${result.dryRun ? "Dry run: " : ""}Detected install metadata: ${result.unitId}`,
795
+ renderChangePlan(result.plan).trimEnd(),
796
+ `Commands: ${formatList(result.detected.commands)}`,
797
+ `Hooks: ${formatList(result.detected.hooks)}`,
798
+ `MCP servers: ${formatList(result.detected.mcpServers)}`,
799
+ `Modified config files: ${formatList(result.detected.modifiedConfigFiles)}`,
800
+ `Changed fields: ${formatList(result.changedFields)}`,
801
+ ""
802
+ ].join("\n");
803
+ }
804
+
805
+ function renderSourceRefresh(result) {
806
+ const lines = [
807
+ `${result.dryRun ? "Dry run: " : ""}Source pins refreshed: ${result.configPath}`,
808
+ renderChangePlan(result.plan).trimEnd(),
809
+ `Refreshed units: ${formatList(result.refreshed.map((unit) => unit.id))}`
810
+ ];
811
+ if (result.skipped.length > 0) {
812
+ lines.push(`Skipped units: ${formatList(result.skipped.map((unit) => `${unit.id}:${unit.reason}`))}`);
813
+ }
814
+ lines.push("");
815
+ return lines.join("\n");
816
+ }
817
+
818
+ function renderChangePlan(plan) {
819
+ const lines = [
820
+ `Changed: ${plan.changed}`,
821
+ `Changed line positions: ${plan.changedLineCount}`
822
+ ];
823
+ if (plan.semanticAvailable) {
824
+ const suffix = plan.semanticTruncated ? " (truncated)" : "";
825
+ lines.push(`Semantic changes: ${plan.semanticChangeCount}${suffix}`);
826
+ for (const change of plan.semanticChanges.slice(0, 10)) {
827
+ lines.push(`- ${change.type} ${change.path}: ${change.before} -> ${change.after}`);
828
+ }
829
+ } else {
830
+ lines.push(`Semantic changes: unavailable (${plan.semanticError})`);
831
+ }
832
+ return `${lines.join("\n")}\n`;
833
+ }
834
+
835
+ function renderApplyAction(result) {
836
+ if (result.mode === "preview") {
837
+ return [
838
+ `Preview action: ${result.action.id}`,
839
+ "Changed: false",
840
+ "Re-run with --yes to apply.",
841
+ ""
842
+ ].join("\n");
843
+ }
844
+ return [
845
+ `Applied action: ${result.action.id}`,
846
+ `Changed: ${result.changed}`,
847
+ "",
848
+ "Returned post-apply brief:",
849
+ "",
850
+ renderSkillBrief(result.brief)
851
+ ].join("\n");
852
+ }
853
+
854
+ function positionalArgs(args, valueOptions = null) {
855
+ const values = [];
856
+ for (let index = 0; index < args.length; index += 1) {
857
+ const arg = args[index];
858
+ if (!arg.startsWith("--")) {
859
+ values.push(arg);
860
+ continue;
861
+ }
862
+ const value = args[index + 1];
863
+ const optionName = arg.slice(2);
864
+ if ((valueOptions === null || valueOptions.has(optionName)) && value !== undefined && !value.startsWith("--")) {
865
+ index += 1;
866
+ }
867
+ }
868
+ return values;
869
+ }
870
+
871
+ function jsonRequested(options, argv) {
872
+ return options.get("json") === "true" || argv.includes("--json");
873
+ }
874
+
875
+ function writeOutput(stdout, value, options, renderText) {
876
+ if (options.get("json") === "true") {
877
+ stdout.write(`${JSON.stringify(value, null, 2)}\n`);
878
+ return;
879
+ }
880
+ stdout.write(renderText());
881
+ }
882
+
883
+ function writeControlResult(stdout, result, options) {
884
+ const warnings = result.policy.warnings;
885
+ if (options.get("json") === "true") {
886
+ stdout.write(`${JSON.stringify({
887
+ message: result.message,
888
+ dryRun: result.dryRun,
889
+ changed: result.changed,
890
+ plan: result.plan,
891
+ warnings
892
+ }, null, 2)}\n`);
893
+ return;
894
+ }
895
+ stdout.write(`${result.dryRun ? "Dry run: " : ""}${result.message}\n`);
896
+ stdout.write(renderChangePlan(result.plan));
897
+ if (warnings.length > 0) {
898
+ stdout.write(`${warnings.join("\n")}\n`);
899
+ }
900
+ }
901
+
902
+ function writeHookInstallError(stdout, options, error, planned) {
903
+ if (options.get("json") === "true") {
904
+ stdout.write(`${JSON.stringify({
905
+ ok: false,
906
+ error,
907
+ ...(planned === undefined ? {} : { planned })
908
+ }, null, 2)}\n`);
909
+ return 1;
910
+ }
911
+ throw new Error(error.message);
912
+ }
913
+
914
+ function hookInstallError(error) {
915
+ const message = error instanceof Error ? error.message : String(error);
916
+ if (message.startsWith("Unknown workflow:")) {
917
+ return { code: "unknown_workflow", message };
918
+ }
919
+ if (message.startsWith("Refusing to overwrite existing hook path:")) {
920
+ return { code: "hook_path_exists", message };
921
+ }
922
+ if (message.startsWith("Unsupported config version:")
923
+ || message.includes("config root")
924
+ || message.includes("must be a")
925
+ || message.includes("Invalid YAML")) {
926
+ return { code: "invalid_config", message };
927
+ }
928
+ if (error?.code === "ENOENT") {
929
+ return { code: "missing_config", message };
930
+ }
931
+ return { code: "hook_install_failed", message };
932
+ }
933
+
934
+ function renderHookInstallDryRun(result) {
935
+ return [
936
+ "Dry run: would install guard hook",
937
+ `Path: ${result.planned.path}`,
938
+ `Workflow: ${result.planned.workflow}`,
939
+ `Command: ${result.planned.command}`,
940
+ `Target exists: ${result.planned.target_exists}`,
941
+ `Planned mode: ${result.planned.planned_mode} (${result.planned.permissions})`,
942
+ `Would be executable: ${result.planned.would_be_executable}`,
943
+ `Preview: ${result.planned.preview.display}`,
944
+ ""
945
+ ].join("\n");
946
+ }
947
+
948
+ function renderList(kind, values) {
949
+ if (values.length === 0) {
950
+ return `${kind}: none\n`;
951
+ }
952
+ if (kind === "skills") {
953
+ return `${values.map((skill) => {
954
+ const roles = skill.workflowRoles.length === 0 ? "none" : skill.workflowRoles.join(",");
955
+ const owner = skill.ownerInstallUnit ?? "direct";
956
+ return `${skill.id}\t${skill.status}\t${skill.invocation}\t${skill.sourceClass}\towner=${owner}\troles=${roles}`;
957
+ }).join("\n")}\n`;
958
+ }
959
+ if (kind === "workflows") {
960
+ return `${values.map((workflow) => {
961
+ return `${workflow.name}\tharness=${workflow.harness}\tactive=${workflow.activeSkills.length}\tblocked=${workflow.blockedSkills.length}\tcapabilities=${workflow.requiredCapabilities.join(",") || "none"}`;
962
+ }).join("\n")}\n`;
963
+ }
964
+ if (kind === "harnesses") {
965
+ return `${values.map((harness) => {
966
+ return `${harness.name}\t${harness.status}\tworkflows=${harness.workflows.join(",") || "none"}\tcommands=${harness.commands.join(",") || "none"}`;
967
+ }).join("\n")}\n`;
968
+ }
969
+ return `${values.map((unit) => {
970
+ return `${unit.id}\t${unit.kind}\t${unit.sourceClass}\tpriority=${unit.priority}\ttrust=${unit.trustLevel}\tenabled=${unit.enabled}\tskills=${unit.skills.length}\trisk=${unit.permissionRisk}`;
971
+ }).join("\n")}\n`;
972
+ }
973
+
974
+ function renderExplain(result) {
975
+ const workflows = result.workflows.length === 0
976
+ ? "none"
977
+ : result.workflows.map((workflow) => `${workflow.workflow}:${workflow.roles.join(",")}`).join(", ");
978
+ const capabilities = result.capabilities.length === 0
979
+ ? "none"
980
+ : result.capabilities.map((capability) => `${capability.name}:${capability.role}`).join(", ");
981
+ return [
982
+ `Skill: ${result.id}`,
983
+ `Status: ${result.status}`,
984
+ `Invocation: ${result.invocation}`,
985
+ `Source: ${result.source.class} (${result.source.detail})`,
986
+ `Trust: ${result.trust.level}${result.trust.pinned ? ", pinned" : ""}${result.trust.signed ? ", signed" : ""}`,
987
+ `Owner install unit: ${result.ownerInstallUnit ?? "direct"}`,
988
+ `Workflows: ${workflows}`,
989
+ `Capabilities: ${capabilities}`,
990
+ `Replaced by: ${result.replacedBy ?? "none"}`,
991
+ ""
992
+ ].join("\n");
993
+ }
994
+
995
+ function renderCanUse(result) {
996
+ const lines = [
997
+ `Allowed: ${result.allowed}`,
998
+ `Automatic allowed: ${result.automaticAllowed}`,
999
+ `Skill: ${result.skill}`,
1000
+ `Workflow: ${result.workflow}`,
1001
+ `Invocation: ${result.invocation ?? "unknown"}`,
1002
+ `Trust: ${result.trust === null ? "unknown" : result.trust.level}`,
1003
+ `Roles: ${result.roles.length === 0 ? "none" : result.roles.join(", ")}`
1004
+ ];
1005
+ if (result.reasons.length > 0) {
1006
+ lines.push("Reasons:");
1007
+ for (const reason of result.reasons) {
1008
+ lines.push(`- ${reason}`);
1009
+ }
1010
+ }
1011
+ lines.push("");
1012
+ return lines.join("\n");
1013
+ }
1014
+
1015
+ function renderSourceAudit(result) {
1016
+ const lines = [
1017
+ `Source audit: ${result.ok ? "passed" : "failed"}`,
1018
+ `Errors: ${result.errors.length}`,
1019
+ `Warnings: ${result.warnings.length}`
1020
+ ];
1021
+ for (const unit of result.units) {
1022
+ const findings = unit.findings.length === 0
1023
+ ? "ok"
1024
+ : unit.findings.map((finding) => `${finding.severity}:${finding.message}`).join("; ");
1025
+ const pinned = unit.pinned ?? unit.digestVerified ?? false;
1026
+ const signed = unit.signed ?? unit.signatureVerified ?? false;
1027
+ const status = unit.status === undefined ? "" : `\tstatus=${unit.status}`;
1028
+ lines.push(`${unit.id}\t${unit.sourceClass}\ttrust=${unit.trustLevel}\trisk=${unit.permissionRisk}\tpinned=${pinned}\tsigned=${signed}${status}\t${findings}`);
1029
+ }
1030
+ lines.push("");
1031
+ return lines.join("\n");
1032
+ }
1033
+
1034
+ function renderRollout(result) {
1035
+ const lines = [
1036
+ `SkillBoard rollout: ${result.status}`,
1037
+ `Command: ${result.command}`,
1038
+ `Exit code: ${result.exitCode}`,
1039
+ `Non-interactive: ${result.nonInteractive}`,
1040
+ `Summary: policyErrors=${result.summary.policyErrors}, sourceErrors=${result.summary.sourceErrors}, sourceWarnings=${result.summary.sourceWarnings}, blockingWarnings=${result.summary.blockingWarnings}`,
1041
+ `Fleet: healthy=${result.fleet.byStatus.healthy}, safe-mode=${result.fleet.byStatus["safe-mode"]}, strict-failed=${result.fleet.byStatus["strict-failed"]}, apply-failed=${result.fleet.byStatus["apply-failed"]}, rollback-needed=${result.fleet.byStatus["rollback-needed"]}`
1042
+ ];
1043
+ if (result.transaction !== undefined) {
1044
+ lines.push(`Transaction: ${result.transaction.state}${result.transaction.id === undefined ? "" : ` ${result.transaction.id}`}`);
1045
+ }
1046
+ if (result.errors.length > 0) {
1047
+ lines.push("Errors:");
1048
+ for (const error of result.errors) {
1049
+ lines.push(`- ${error}`);
1050
+ }
1051
+ }
1052
+ lines.push("");
1053
+ return lines.join("\n");
1054
+ }
1055
+
1056
+ function renderDoctorSummary(result) {
1057
+ const status = result.ok ? result.reviewRequired ? "safe mode, review needed" : "passed" : "needs attention";
1058
+ const lines = [
1059
+ `SkillBoard doctor: ${status}`,
1060
+ `Workspace: ${result.workspace.skills.declared} skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
1061
+ `Source audit: ${result.sources.ok ? "passed" : "failed"} (${result.sources.errors.length} errors, ${result.sources.warnings.length} warnings, ${result.sources.blockingWarnings.length} blocking)`,
1062
+ `Policy: ${result.policy.ok ? "passed" : "failed"} (${result.policy.errors.length} errors, ${result.policy.warnings.length} warnings)`
1063
+ ];
1064
+ const concerns = [
1065
+ ...result.sources.blockingWarnings.slice(0, 3).map((w) => `Blocking: ${w}`),
1066
+ ...result.sources.warnings.slice(0, 3).map((w) => `Warning: ${w}`),
1067
+ ...result.policy.errors.slice(0, 3).map((e) => `Policy error: ${e}`),
1068
+ ...result.policy.warnings.slice(0, 3).map((w) => `Policy warning: ${w}`)
1069
+ ];
1070
+ if (concerns.length > 0) {
1071
+ lines.push("Top concerns:");
1072
+ for (const concern of concerns) {
1073
+ lines.push(`- ${concern}`);
1074
+ }
1075
+ }
1076
+ if (result.recommendations.length > 0) {
1077
+ lines.push("Recommendations:");
1078
+ for (const recommendation of result.recommendations.slice(0, 3)) {
1079
+ lines.push(`- ${recommendation}`);
1080
+ }
1081
+ }
1082
+ lines.push("");
1083
+ return lines.join("\n");
1084
+ }
1085
+
1086
+ function renderDoctor(result) {
1087
+ const bridges = result.bridges.map((bridge) => `${bridge.file}=${bridge.status}`).join(", ");
1088
+ const sourceMode = result.sources.verified ? "verified" : "audit";
1089
+ const status = result.ok ? result.reviewRequired ? "safe mode, review needed" : "passed" : "needs attention";
1090
+ const lines = [
1091
+ `SkillBoard doctor: ${status}`,
1092
+ `Root: ${result.root}`,
1093
+ `Config: ${result.config.exists ? result.config.valid ? `valid v${result.config.version}` : `invalid (${result.config.error})` : "missing"}`,
1094
+ `Bridge: ${bridges}`,
1095
+ `Workspace: ${result.workspace.skills.declared} declared skills, ${result.workspace.skills.installed} installed skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
1096
+ `Skill states: ${renderCounts(result.workspace.skills.byStatus)}`,
1097
+ `Invocations: ${renderCounts(result.workspace.skills.byInvocation)}`,
1098
+ `Source classes: ${renderCounts(result.workspace.installUnits.bySourceClass)}`,
1099
+ `Source ${sourceMode}: ${result.sources.ok ? "passed" : "failed"} (${result.sources.errors.length} errors, ${result.sources.warnings.length} warnings, ${result.sources.blockingWarnings.length} blocking warnings)`,
1100
+ `Policy: ${result.policy.ok ? "passed" : "failed"} (${result.policy.errors.length} errors, ${result.policy.warnings.length} warnings)`,
1101
+ `Strict gate: ${result.strictOk ? "passed" : "review needed"}`,
1102
+ `Review required: ${result.reviewRequired}`,
1103
+ `High-risk install units: ${formatList(result.workspace.installUnits.highRisk)}`,
1104
+ `Runtime extension units: ${formatList(result.workspace.installUnits.runtimeExtensions)}`,
1105
+ `Uninstall dry run: remove ${result.uninstall.removed.length}, update ${result.uninstall.updated.length}, preserve ${result.uninstall.preserved.length}`
1106
+ ];
1107
+ if (result.recommendations.length > 0) {
1108
+ lines.push("Recommendations:");
1109
+ for (const recommendation of result.recommendations) {
1110
+ lines.push(`- ${recommendation}`);
1111
+ }
1112
+ }
1113
+ lines.push("");
1114
+ return lines.join("\n");
1115
+ }
1116
+
1117
+ function renderCounts(counts) {
1118
+ const entries = Object.entries(counts).filter(([, count]) => count > 0);
1119
+ return entries.length === 0
1120
+ ? "none"
1121
+ : entries.map(([key, count]) => `${key}=${count}`).join(", ");
1122
+ }
1123
+
1124
+ function helpText() {
1125
+ return [
1126
+ "SkillBoard - workflow-scoped agent skill policy",
1127
+ "",
1128
+ "Commands:",
1129
+ " init [--dir <path>] [--scan-root <dir>[,<dir>]] [--no-scan-installed]",
1130
+ " uninstall [--dir <path>] [--dry-run] [--remove-config|--reset-config] [--remove-reports] [--remove-hooks] [--keep-empty-dirs]",
1131
+ " inventory refresh [--dir <path>] [--config <path>] [--scan-root <dir>[,<dir>]] [--dry-run] [--json]",
1132
+ " inventory detect --unit <id> --config <path> [--install-output <path>] [--config-file a,b] [--source <value>] [--kind <kind>] [--scope <scope>] [--dry-run] [--json]",
1133
+ " sources refresh [--dir <path>] [--config <path>] [--unit <id>[,<id>]] [--cache-dir <dir>] [--dry-run] [--json]",
1134
+ " doctor [--dir <path>] [--config <path>] [--skills <dir>] [--verify] [--strict] [--summary] [--json]",
1135
+ " 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]",
1137
+ " apply-action <action-id> [--workflow <name>] [--dir <path>] [--config <path>] [--skills <dir>] [--dry-run] [--yes] [--allow-destructive] [--json]",
1138
+ " import --profile <id-or-path> --source-root <dir> [--profile-dirs a,b] [--out <path>]",
1139
+ " import --profile <id-or-path> --source-root <dir> --config <path> --merge [--replace] [--dry-run]",
1140
+ " scan --config <path>",
1141
+ " check --config <path> --skills <dir>",
1142
+ " list [skills|workflows|harnesses|install-units] --config <path> --skills <dir> [--workflow <name>] [--json]",
1143
+ " explain <skill-id> --config <path> --skills <dir> [--json]",
1144
+ " can-use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1145
+ " guard use <skill-id> --workflow <name> --config <path> --skills <dir> [--json]",
1146
+ " audit sources --config <path> --skills <dir> [--verify] [--json]",
1147
+ " rollout [audit|plan|apply|rollback|report] [--dir <path>] [--config <path>] [--skills <dir>] [--transaction <id>] [--json]",
1148
+ " hook install --workflow <name> --config <path> --skills <dir> [--out <path>] [--skillboard-bin <path>] [--dry-run] [--json]",
1149
+ " lock write --config <path> --skills <dir> [--out <path>] [--replace] [--allow-unverified] [--json]",
1150
+ " review install-unit <unit-id> [--trust-level trusted|reviewed|unreviewed|blocked] --config <path> --skills <dir> [--dry-run] [--json]",
1151
+ " 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
+ " add workflow <workflow-name> --harness <harness-name> --config <path> --skills <dir> [--skill <id>[,<id>]] [--harness-status <status>] [--require-existing-harness] [--dry-run] [--json]",
1153
+ " add harness <harness-name> --config <path> --skills <dir> [--status <status>] [--command <cmd>[,<cmd>]] [--dry-run] [--json]",
1154
+ " activate <skill-id> --workflow <name> [--mode manual-only|router-only|workflow-auto] --config <path> --skills <dir> [--dry-run] [--json]",
1155
+ " block <skill-id> --workflow <name> --config <path> --skills <dir> [--dry-run] [--json]",
1156
+ " quarantine <skill-id> --config <path> --skills <dir> [--dry-run] [--json]",
1157
+ " prefer <skill-id> --workflow <name> --capability <name> --config <path> --skills <dir> [--dry-run] [--json]",
1158
+ " remove skill <skill-id> --config <path> --skills <dir> [--force] [--dry-run] [--json]",
1159
+ " dashboard --config <path> --skills <dir> [--out <path>]",
1160
+ " reconcile --config <path> --skills <dir> [--actual-harnesses a,b] [--out <path>]",
1161
+ " impact disable <skill-id> --config <path> --skills <dir> [--out <path>] [--json]",
1162
+ "",
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.",
1168
+ " 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
+ ""
1170
+ ].join("\n");
1171
+ }