agent-skillboard 0.2.18 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +161 -255
  3. package/bin/postinstall.mjs +2 -1
  4. package/docs/adapters.md +37 -113
  5. package/docs/ai-skill-routing-goal.md +41 -108
  6. package/docs/capabilities.md +17 -104
  7. package/docs/install.md +70 -473
  8. package/docs/policy-model.md +50 -280
  9. package/docs/positioning.md +19 -86
  10. package/docs/profiles.md +21 -153
  11. package/docs/reference.md +133 -362
  12. package/docs/rollout-runbook.md +23 -25
  13. package/docs/routing.md +23 -90
  14. package/docs/user-flow.md +68 -279
  15. package/docs/value-proof.md +23 -181
  16. package/docs/variant-lifecycle.md +47 -67
  17. package/docs/versioning.md +49 -269
  18. package/examples/v2-multi-source.config.yaml +35 -0
  19. package/examples/v2-policy-error.config.yaml +6 -0
  20. package/package.json +1 -1
  21. package/src/advisor/actions.mjs +102 -6
  22. package/src/advisor/application-commands.mjs +10 -9
  23. package/src/advisor/apply-action.mjs +74 -1
  24. package/src/advisor/guidance.mjs +24 -16
  25. package/src/advisor/schema.mjs +17 -6
  26. package/src/advisor/skills.mjs +18 -5
  27. package/src/advisor.mjs +27 -9
  28. package/src/agent-integration-cli.mjs +96 -13
  29. package/src/agent-integration-content.mjs +21 -11
  30. package/src/agent-integration-files.mjs +1 -1
  31. package/src/agent-integration-home.mjs +14 -1
  32. package/src/agent-inventory-platforms.mjs +21 -8
  33. package/src/agent-inventory.mjs +44 -16
  34. package/src/agent-root-registry.mjs +127 -0
  35. package/src/agent-skill-import.mjs +2 -2
  36. package/src/agent-skill-roots.mjs +70 -13
  37. package/src/audit-paths.mjs +42 -0
  38. package/src/brief-cli.mjs +3 -2
  39. package/src/brief-renderer.mjs +1 -0
  40. package/src/cli.mjs +521 -235
  41. package/src/compatibility.mjs +24 -0
  42. package/src/control/can-use-guard.mjs +21 -1
  43. package/src/control/config-write.mjs +32 -2
  44. package/src/control/skill-crud.mjs +5 -0
  45. package/src/control/v2-guard.mjs +175 -0
  46. package/src/control/v2-skill-crud.mjs +32 -0
  47. package/src/control/v2-skill-forget.mjs +38 -0
  48. package/src/control/variant-status.mjs +47 -1
  49. package/src/control.mjs +55 -0
  50. package/src/doctor.mjs +71 -5
  51. package/src/domain/v2-policy.mjs +111 -0
  52. package/src/hook-plan.mjs +33 -3
  53. package/src/impact.mjs +52 -29
  54. package/src/index.mjs +25 -1
  55. package/src/init.mjs +50 -34
  56. package/src/install-health.mjs +177 -0
  57. package/src/inventory-install-units.mjs +63 -0
  58. package/src/inventory-json.mjs +279 -0
  59. package/src/inventory-refresh.mjs +168 -19
  60. package/src/lifecycle-cli.mjs +40 -12
  61. package/src/lifecycle-content.mjs +52 -67
  62. package/src/migration/v1-to-v2.mjs +212 -0
  63. package/src/migration/v2-files.mjs +211 -0
  64. package/src/migration/v2-journal.mjs +169 -0
  65. package/src/migration/v2-projection.mjs +108 -0
  66. package/src/migration/v2-transaction.mjs +205 -0
  67. package/src/policy.mjs +3 -0
  68. package/src/reconcile.mjs +139 -111
  69. package/src/report.mjs +168 -148
  70. package/src/review.mjs +2 -0
  71. package/src/route-advisory.mjs +47 -2
  72. package/src/route-selection.mjs +38 -2
  73. package/src/route.mjs +62 -2
  74. package/src/shared-skill-reconcile.mjs +97 -0
  75. package/src/shared-skill.mjs +325 -0
  76. package/src/source-digest.mjs +42 -0
  77. package/src/source-profiles.mjs +171 -144
  78. package/src/source-verification.mjs +32 -48
  79. package/src/uninstall.mjs +22 -0
  80. package/src/user-state-paths.mjs +19 -0
  81. package/src/user-uninstall.mjs +161 -0
  82. package/src/workspace.mjs +119 -79
@@ -0,0 +1,161 @@
1
+ import { lstat, readdir, readFile, rm } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { uninstallAgentIntegration } from "./agent-integration-files.mjs";
4
+ import { resolveSetupHome } from "./agent-integration-home.mjs";
5
+ import {
6
+ agentSkillRootCandidates,
7
+ supportedAgentNames
8
+ } from "./agent-skill-roots.mjs";
9
+
10
+ const SHARE_MARKER = ".skillboard-share.json";
11
+
12
+ export async function uninstallUser(options = {}) {
13
+ const env = options.env ?? process.env;
14
+ const home = await resolveSetupHome(env, options.runtime ?? {});
15
+ const planned = await planUserUninstall(home, env);
16
+ if (options.dryRun) {
17
+ const guidance = await uninstallAgentIntegration(planned.guidanceTargets, true);
18
+ return result(home, true, planned.managedCopies, guidance, planned.statePaths, planned.preserved);
19
+ }
20
+
21
+ const removedCopies = [];
22
+ const preserved = [...planned.preserved];
23
+ for (const copy of planned.managedCopies) {
24
+ if (await removeManagedCopy(copy)) {
25
+ removedCopies.push(copy);
26
+ } else {
27
+ preserved.push(copy.path);
28
+ }
29
+ }
30
+ const guidance = await uninstallAgentIntegration(planned.guidanceTargets, false);
31
+ const removedState = [];
32
+ for (const state of planned.statePaths) {
33
+ if (await removeStatePath(state)) removedState.push(state);
34
+ else preserved.push(state);
35
+ }
36
+ return result(home, false, removedCopies, guidance, removedState, preserved);
37
+ }
38
+
39
+ async function planUserUninstall(home, env) {
40
+ const preserved = [];
41
+ const roots = [{ root: join(home, ".agents", "shared-skills"), agent: null }];
42
+ for (const agent of supportedAgentNames()) {
43
+ for (const candidate of await agentSkillRootCandidates(agent, home, env)) {
44
+ roots.push({ root: candidate.skillRoot, agent });
45
+ }
46
+ }
47
+ const managedCopies = [];
48
+ const seenRoots = new Set();
49
+ for (const entry of roots) {
50
+ const root = resolve(entry.root);
51
+ if (seenRoots.has(root)) continue;
52
+ seenRoots.add(root);
53
+ managedCopies.push(...await managedCopiesInRoot(root, entry.agent, preserved));
54
+ }
55
+
56
+ const guidanceTargets = [];
57
+ for (const agent of supportedAgentNames()) {
58
+ for (const candidate of await agentSkillRootCandidates(agent, home, env)) {
59
+ guidanceTargets.push({
60
+ agent,
61
+ home,
62
+ skillPath: join(candidate.skillRoot, "skillboard", "SKILL.md"),
63
+ root: candidate.skillRoot,
64
+ source: candidate.source
65
+ });
66
+ }
67
+ }
68
+ return {
69
+ managedCopies: managedCopies.sort((left, right) => left.path.localeCompare(right.path)),
70
+ guidanceTargets,
71
+ statePaths: await existingStatePaths(home),
72
+ preserved: [...new Set(preserved)].sort()
73
+ };
74
+ }
75
+
76
+ async function managedCopiesInRoot(root, agent, preserved) {
77
+ const stats = await pathStats(root);
78
+ if (stats === null) return [];
79
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
80
+ preserved.push(root);
81
+ return [];
82
+ }
83
+ const copies = [];
84
+ const entries = await readdir(root, { withFileTypes: true });
85
+ for (const entry of entries) {
86
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
87
+ const path = join(root, entry.name);
88
+ const marker = await readManagedMarker(path, entry.name, agent);
89
+ if (marker !== null) copies.push({ path, skill: entry.name, agent, mode: marker.mode });
90
+ }
91
+ return copies;
92
+ }
93
+
94
+ async function readManagedMarker(path, skill, agent) {
95
+ const markerPath = join(path, SHARE_MARKER);
96
+ const stats = await pathStats(markerPath);
97
+ if (stats === null || stats.isSymbolicLink() || !stats.isFile()) return null;
98
+ const value = await readFile(markerPath, "utf8").then(parseJsonOrNull, () => null);
99
+ if (value?.version !== 1 || value.managed_by !== "skillboard" || value.skill !== skill) return null;
100
+ if (agent === null && value.mode === "shared-source") return value;
101
+ if (agent !== null && value.mode === "agent-copy" && value.target_agent === agent) return value;
102
+ return null;
103
+ }
104
+
105
+ function parseJsonOrNull(text) {
106
+ try {
107
+ return JSON.parse(text);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ async function removeManagedCopy(copy) {
114
+ const stats = await pathStats(copy.path);
115
+ if (stats === null) return true;
116
+ if (stats.isSymbolicLink() || !stats.isDirectory()) return false;
117
+ const marker = await readManagedMarker(copy.path, copy.skill, copy.agent);
118
+ if (marker === null || marker.mode !== copy.mode) return false;
119
+ await rm(copy.path, { recursive: true, force: true });
120
+ return true;
121
+ }
122
+
123
+ async function existingStatePaths(home) {
124
+ const result = [];
125
+ for (const path of [join(home, "skillboard.config.yaml"), join(home, ".skillboard")]) {
126
+ if (await pathStats(path) !== null) result.push(path);
127
+ }
128
+ return result;
129
+ }
130
+
131
+ async function removeStatePath(path) {
132
+ const stats = await pathStats(path);
133
+ if (stats === null) return true;
134
+ const name = basename(path);
135
+ const supported = name === "skillboard.config.yaml"
136
+ ? stats.isFile() || stats.isSymbolicLink()
137
+ : name === ".skillboard" && (stats.isDirectory() || stats.isSymbolicLink());
138
+ if (!supported) return false;
139
+ await rm(path, { recursive: stats.isDirectory(), force: true });
140
+ return true;
141
+ }
142
+
143
+ function result(home, dryRun, managedCopies, guidance, statePaths, preserved) {
144
+ return {
145
+ ok: true,
146
+ mode: "user",
147
+ dry_run: dryRun,
148
+ home,
149
+ managed_copies: managedCopies,
150
+ guidance,
151
+ state_paths: statePaths,
152
+ preserved: [...new Set([...preserved, ...guidance.preserved])].sort()
153
+ };
154
+ }
155
+
156
+ async function pathStats(path) {
157
+ return await lstat(path).catch((error) => {
158
+ if (error?.code === "ENOENT") return null;
159
+ throw error;
160
+ });
161
+ }
package/src/workspace.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import YAML from "yaml";
4
+ import YAML from "yaml";
5
5
  import {
6
6
  readBoolean,
7
7
  readOptionalRecord,
@@ -19,16 +19,26 @@ import {
19
19
  } from "./domain/constants.mjs";
20
20
  import { parseInstallUnits } from "./install-units.mjs";
21
21
  import { normalizeSkillPath } from "./skill-paths.mjs";
22
+ import { parseV2Policy, serializeV2Policy } from "./domain/v2-policy.mjs";
23
+ import { loadV2InventoryIndex } from "./control/v2-guard.mjs";
24
+ import { compatibilityForVersion } from "./compatibility.mjs";
22
25
 
26
+ export { serializeV2Policy };
27
+
28
+ /** @returns {Promise<any>} */
23
29
  export async function loadWorkspace(options) {
24
- const configText = await readFile(options.configPath, "utf8");
25
- const parsed = YAML.parse(configText);
30
+ const configText = await readFile(options.configPath, "utf8");
31
+ const parsed = YAML.parse(configText);
26
32
  const config = requireRecord(parsed, "config root");
27
33
  const version = parseVersion(config.version);
34
+ if (version === 2) {
35
+ return loadV2Workspace(config, options);
36
+ }
28
37
  const skills = parseSkills(config.skills);
29
38
  const installUnits = parseInstallUnits(config.install_units);
30
39
  return {
31
40
  version,
41
+ compatibility: compatibilityForVersion(version),
32
42
  defaults: parseDefaults(config.defaults),
33
43
  installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
34
44
  configPath: options.configPath,
@@ -44,6 +54,29 @@ export async function loadWorkspace(options) {
44
54
  };
45
55
  }
46
56
 
57
+ async function loadV2Workspace(config, options) {
58
+ const { workflows, skills } = parseV2Policy(config);
59
+ const inventoryPath = options.inventoryPath ?? join(dirname(options.configPath), ".skillboard", "inventory.json");
60
+ const inventory = await loadV2InventoryIndex(inventoryPath);
61
+ return {
62
+ version: 2,
63
+ compatibility: null,
64
+ defaults: {},
65
+ installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
66
+ configPath: options.configPath,
67
+ env: options.env ?? process.env,
68
+ home: options.home,
69
+ installUnits: []
70
+ }),
71
+ skills,
72
+ capabilities: [],
73
+ harnesses: [],
74
+ installUnits: inventory.installUnits ?? [],
75
+ workflows,
76
+ inventory
77
+ };
78
+ }
79
+
47
80
  async function discoverInstalledSkills(skillsRoot, declaredSkills, options = {}) {
48
81
  const installed = [];
49
82
  const installedKeys = new Set();
@@ -123,7 +156,7 @@ function resolveStoredPath(value, options) {
123
156
  }
124
157
  return resolve(dirname(options.configPath ?? "."), value);
125
158
  }
126
-
159
+
127
160
  async function findSkillFiles(root, seen = new Set()) {
128
161
  const files = [];
129
162
  const resolvedRoot = await realpath(root).catch(() => root);
@@ -148,8 +181,8 @@ async function findSkillFiles(root, seen = new Set()) {
148
181
  }
149
182
  }
150
183
  return files;
151
- }
152
-
184
+ }
185
+
153
186
  function parseSkillFrontmatter(text) {
154
187
  const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
155
188
  if (match?.groups === undefined) {
@@ -170,49 +203,56 @@ function parseSkillFrontmatter(text) {
170
203
  description: readString(raw, "description", "")
171
204
  };
172
205
  }
173
-
174
- function parseVersion(value) {
175
- if (value === undefined) {
176
- return 1;
177
- }
178
- if (value !== 1) {
179
- throw new Error(`Unsupported config version: ${value}`);
180
- }
181
- return value;
182
- }
183
-
184
- function parseDefaults(value) {
185
- const raw = requireRecord(value ?? {}, "defaults");
186
- return {
187
- invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
188
- allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
189
- requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
190
- };
191
- }
192
-
193
- function parseSkills(value) {
194
- const raw = requireRecord(value ?? {}, "skills");
195
- return Object.entries(raw).map(([id, entry]) => {
196
- const skill = requireRecord(entry, `skills.${id}`);
197
- const status = readString(skill, "status", "vendor");
198
- const invocation = readString(skill, "invocation", "manual-only");
199
- const exposure = readString(skill, "exposure", "exported");
200
- if (!STATUS_VALUES.has(status)) {
201
- throw new Error(`Unsupported status for ${id}: ${status}`);
202
- }
203
- if (!INVOCATION_VALUES.has(invocation)) {
204
- throw new Error(`Unsupported invocation for ${id}: ${invocation}`);
205
- }
206
- if (!EXPOSURE_VALUES.has(exposure)) {
207
- throw new Error(`Unsupported exposure for ${id}: ${exposure}`);
208
- }
206
+
207
+ function parseVersion(value) {
208
+ if (value === undefined) {
209
+ return 1;
210
+ }
211
+ if (value !== 1 && value !== 2) {
212
+ throw new Error(`Unsupported config version: ${value}`);
213
+ }
214
+ return value;
215
+ }
216
+
217
+ function parseDefaults(value) {
218
+ const raw = requireRecord(value ?? {}, "defaults");
219
+ return {
220
+ invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
221
+ allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
222
+ requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
223
+ };
224
+ }
225
+
226
+ function parseSkills(value) {
227
+ const raw = requireRecord(value ?? {}, "skills");
228
+ return Object.entries(raw).map(([id, entry]) => {
229
+ const skill = requireRecord(entry, `skills.${id}`);
230
+ const v2Keys = ["enabled", "shared", "preference"].filter((key) => Object.prototype.hasOwnProperty.call(skill, key));
231
+ if (v2Keys.length > 0) {
232
+ throw new Error(
233
+ `skills.${id} uses version 2 key ${v2Keys.join(", ")} in a version 1 config. ` +
234
+ "Run `skillboard migrate v2` to convert the complete policy."
235
+ );
236
+ }
237
+ const status = readString(skill, "status", "vendor");
238
+ const invocation = readString(skill, "invocation", "manual-only");
239
+ const exposure = readString(skill, "exposure", "exported");
240
+ if (!STATUS_VALUES.has(status)) {
241
+ throw new Error(`Unsupported status for ${id}: ${status}`);
242
+ }
243
+ if (!INVOCATION_VALUES.has(invocation)) {
244
+ throw new Error(`Unsupported invocation for ${id}: ${invocation}`);
245
+ }
246
+ if (!EXPOSURE_VALUES.has(exposure)) {
247
+ throw new Error(`Unsupported exposure for ${id}: ${exposure}`);
248
+ }
209
249
  return {
210
250
  id,
211
251
  path: normalizeSkillPath(readString(skill, "path", id), `skills.${id}.path`),
212
- status,
213
- invocation,
214
- exposure,
215
- category: readString(skill, "category", "uncategorized"),
252
+ status,
253
+ invocation,
254
+ exposure,
255
+ category: readString(skill, "category", "uncategorized"),
216
256
  canonicalFor: readStringList(skill, "canonical_for"),
217
257
  conflictsWith: readStringList(skill, "conflicts_with"),
218
258
  replacedBy: readOptionalString(skill, "replaced_by"),
@@ -266,39 +306,39 @@ function parseCapabilities(value) {
266
306
  };
267
307
  });
268
308
  }
269
-
270
- function parseHarnesses(value) {
271
- const raw = requireRecord(value ?? {}, "harnesses");
272
- return Object.entries(raw).map(([name, entry]) => {
273
- const harness = requireRecord(entry, `harnesses.${name}`);
274
- const status = readString(harness, "status", "available");
275
- if (!HARNESS_STATUS_VALUES.has(status)) {
276
- throw new Error(`Unsupported harness status for ${name}: ${status}`);
277
- }
278
- return {
279
- name,
280
- status,
281
- workflows: readStringList(harness, "workflows"),
282
- commands: readStringList(harness, "commands")
283
- };
284
- });
285
- }
286
-
287
- function parseWorkflows(value) {
288
- const raw = requireRecord(value ?? {}, "workflows");
289
- return Object.entries(raw).map(([name, entry]) => {
290
- const workflow = requireRecord(entry, `workflows.${name}`);
291
- return {
292
- name,
293
- harness: readString(workflow, "harness", "unspecified"),
294
- activeSkills: readStringList(workflow, "active_skills"),
295
- blockedSkills: readStringList(workflow, "blocked_skills"),
296
- requiredOutputs: readStringList(workflow, "required_outputs"),
297
- requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
298
- };
299
- });
300
- }
301
-
309
+
310
+ function parseHarnesses(value) {
311
+ const raw = requireRecord(value ?? {}, "harnesses");
312
+ return Object.entries(raw).map(([name, entry]) => {
313
+ const harness = requireRecord(entry, `harnesses.${name}`);
314
+ const status = readString(harness, "status", "available");
315
+ if (!HARNESS_STATUS_VALUES.has(status)) {
316
+ throw new Error(`Unsupported harness status for ${name}: ${status}`);
317
+ }
318
+ return {
319
+ name,
320
+ status,
321
+ workflows: readStringList(harness, "workflows"),
322
+ commands: readStringList(harness, "commands")
323
+ };
324
+ });
325
+ }
326
+
327
+ function parseWorkflows(value) {
328
+ const raw = requireRecord(value ?? {}, "workflows");
329
+ return Object.entries(raw).map(([name, entry]) => {
330
+ const workflow = requireRecord(entry, `workflows.${name}`);
331
+ return {
332
+ name,
333
+ harness: readString(workflow, "harness", "unspecified"),
334
+ activeSkills: readStringList(workflow, "active_skills"),
335
+ blockedSkills: readStringList(workflow, "blocked_skills"),
336
+ requiredOutputs: readStringList(workflow, "required_outputs"),
337
+ requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
338
+ };
339
+ });
340
+ }
341
+
302
342
  function parseRequiredCapabilities(value, label) {
303
343
  const raw = requireRecord(value ?? {}, label);
304
344
  return Object.entries(raw).map(([name, entry]) => {