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
@@ -1,9 +1,9 @@
1
1
  import { command } from "./action-core.mjs";
2
2
 
3
- const GUARD_WHEN = "before invoking a skill";
3
+ const GUARD_WHEN = "immediately before skill use";
4
4
  const GOAL_DOCUMENT = Object.freeze({
5
5
  path: "docs/ai-skill-routing-goal.md",
6
- purpose: "Preserve SkillBoard as a permissive AI skill routing layer: keep skills broadly available, resolve overlaps deterministically, explain briefly, ask after use when policy learning helps, and remember usage policy without rewriting skill bodies.",
6
+ purpose: "Preserve SkillBoard v2 as a user-level control plane: enable or disable a skill, opt individual skills into cross-agent sharing, and use optional preference only to rank available overlaps.",
7
7
  loop: Object.freeze([
8
8
  "observe",
9
9
  "route",
@@ -18,7 +18,7 @@ const GOAL_DOCUMENT = Object.freeze({
18
18
  "before changing brief output",
19
19
  "before changing bridge instructions",
20
20
  "before changing policy UX",
21
- "before changing workflow UX"
21
+ "before changing sharing UX"
22
22
  ])
23
23
  });
24
24
  const GUARD_ALLOWED_USE = Object.freeze({
@@ -71,6 +71,9 @@ function guidanceStatus(brief) {
71
71
  if (brief.error?.code === "unknown-workflow" || brief.workflow?.unknown === true) {
72
72
  return "unknown-workflow";
73
73
  }
74
+ if (brief.health?.config?.version === 2 && brief.workflow?.selected === null && (brief.workflow?.candidates?.length ?? 0) === 0) {
75
+ return "ready";
76
+ }
74
77
  if (brief.workflow?.needs_selection === true || brief.workflow?.selected === null) {
75
78
  return "workflow-selection-needed";
76
79
  }
@@ -92,20 +95,20 @@ function hasInvalidConfig(brief) {
92
95
  }
93
96
 
94
97
  function summaryForStatus(status, brief) {
95
- const readyCount = (brief.skills?.automatic_allowed?.length ?? 0) + (brief.skills?.manual_allowed?.length ?? 0);
98
+ const readyCount = availableSkillCount(brief);
96
99
  const decisionCount = guidanceDecisionCount(brief);
97
100
  const blockedCount = brief.skills?.blocked?.length ?? 0;
98
101
  switch (status) {
99
102
  case "ready":
100
- return `SkillBoard is ready; ${readyCount} skills are available in this workflow.`;
103
+ return `SkillBoard is ready; ${readyCount} skills are available for the current agent.`;
101
104
  case "needs-decision":
102
- return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before this workflow is fully ready.`;
105
+ return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before the requested policy change is applied.`;
103
106
  case "blocked":
104
107
  return `SkillBoard found blocking policy issues; ${blockedCount} skills are blocked for safety.`;
105
108
  case "not-initialized":
106
- return "SkillBoard is not initialized in this project.";
109
+ return "SkillBoard user state has not been set up yet.";
107
110
  case "invalid-config":
108
- return "SkillBoard cannot read the project configuration.";
111
+ return "SkillBoard cannot read the selected user policy configuration.";
109
112
  case "workflow-selection-needed":
110
113
  return "SkillBoard needs a workflow selection before applying action cards.";
111
114
  case "unknown-workflow":
@@ -115,6 +118,10 @@ function summaryForStatus(status, brief) {
115
118
  }
116
119
  }
117
120
 
121
+ function availableSkillCount(brief) {
122
+ return (brief.skills?.automatic_allowed?.length ?? 0) + (brief.skills?.manual_allowed?.length ?? 0);
123
+ }
124
+
118
125
  function guidanceDecisionCount(brief) {
119
126
  const skillDecisionCount = brief.skills?.needs_review?.length ?? 0;
120
127
  if (skillDecisionCount > 0) {
@@ -137,7 +144,9 @@ function recommendedNextStep(status, brief, choices, route = null) {
137
144
  ? "Ask a clarifying question; no workflow capability matched this request."
138
145
  : `Use ${route.recommended_skill} for this request after the guard check passes${postUsePolicyStep(route)}.`;
139
146
  }
140
- return "Run the guard check before invoking any selected skill.";
147
+ return availableSkillCount(brief) === 0
148
+ ? "No policy change is required; valid installed skills default to enabled and agent-local."
149
+ : "Run the guard check immediately before using any selected skill.";
141
150
  case "needs-decision":
142
151
  if (route?.recommended_skill !== null && route?.guard_allowed === true) {
143
152
  return route.post_use_policy_suggestion === null
@@ -156,7 +165,7 @@ function recommendedNextStep(status, brief, choices, route = null) {
156
165
  : `Review the blocked item before applying: ${firstChoice.label}.`;
157
166
  case "not-initialized":
158
167
  return firstChoice === undefined
159
- ? "Initialize SkillBoard before checking skill availability."
168
+ ? "Run SkillBoard setup before checking skill availability."
160
169
  : `Ask the user whether to approve: ${firstChoice.label}.`;
161
170
  case "invalid-config":
162
171
  return "Fix the SkillBoard configuration before checking skill availability.";
@@ -237,16 +246,15 @@ function confirmableAction(action) {
237
246
  }
238
247
 
239
248
  function guardCommandHint(brief) {
240
- if (hasInvalidConfig(brief)) {
241
- return null;
242
- }
243
- if (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true) {
249
+ if (hasInvalidConfig(brief) || brief.error?.code === "not-initialized" || availableSkillCount(brief) === 0) {
244
250
  return null;
245
251
  }
252
+ const version = brief.health?.config?.version;
253
+ if (version === 1 && (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true)) return null;
246
254
  return command([
247
255
  "skillboard", "guard", "use", "<skill-id>",
248
- "--workflow", brief.workflow.selected,
256
+ ...(version === 1 ? ["--workflow", brief.workflow.selected] : []),
249
257
  "--config", brief.health.config_path,
250
- "--skills", brief.health.skills_root
258
+ ...(brief.health.skills_root === undefined || brief.health.skills_root === null ? [] : ["--skills", brief.health.skills_root])
251
259
  ]).display;
252
260
  }
@@ -1,4 +1,4 @@
1
- import { isAbsolute, resolve } from "node:path";
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
2
  import { uninstallProject } from "../uninstall.mjs";
3
3
  import { buildAssistantGuidance } from "./guidance.mjs";
4
4
  import { sortedStrings } from "./sort.mjs";
@@ -18,6 +18,7 @@ export function buildBrief(data) {
18
18
  ok: data.ok,
19
19
  schema_version: SCHEMA_VERSION
20
20
  };
21
+ if (data.compatibility !== undefined && data.compatibility !== null) brief.compatibility = data.compatibility;
21
22
  if (data.error !== undefined) {
22
23
  brief.error = data.error;
23
24
  }
@@ -35,11 +36,12 @@ export function buildBrief(data) {
35
36
  }
36
37
 
37
38
  export function resolveProjectPaths(options) {
38
- const root = resolve(options.root ?? ".");
39
+ const configPath = resolve(options.configPath ?? "skillboard.config.yaml");
40
+ const root = resolve(options.root ?? dirname(configPath));
39
41
  return {
40
42
  root,
41
- configPath: resolveUnderRoot(root, options.configPath ?? "skillboard.config.yaml"),
42
- skillsRoot: resolveUnderRoot(root, options.skillsRoot ?? "skills")
43
+ configPath: resolveUnderRoot(root, configPath),
44
+ skillsRoot: options.skillsRoot === undefined ? undefined : resolveUnderRoot(root, options.skillsRoot)
43
45
  };
44
46
  }
45
47
 
@@ -66,14 +68,14 @@ export async function buildCleanup(root) {
66
68
  }
67
69
 
68
70
  export function healthFromDoctor(doctor, paths) {
69
- return {
71
+ const health = {
70
72
  mode: doctor.mode,
71
73
  review_required: doctor.reviewRequired,
72
74
  strict_ok: doctor.strictOk,
73
75
  initialized: doctor.initialized,
74
76
  root: paths.root,
75
77
  config_path: paths.configPath,
76
- skills_root: paths.skillsRoot,
78
+ skills_root: paths.skillsRoot ?? null,
77
79
  config: {
78
80
  exists: doctor.config.exists,
79
81
  valid: doctor.config.valid,
@@ -86,6 +88,15 @@ export function healthFromDoctor(doctor, paths) {
86
88
  warnings: sortedStrings(doctor.policy.warnings)
87
89
  }
88
90
  };
91
+ if (doctor.inventory.required) {
92
+ health.inventory = {
93
+ required: true,
94
+ ok: doctor.inventory.ok,
95
+ path: doctor.inventory.path,
96
+ errors: sortedStrings(doctor.inventory.errors)
97
+ };
98
+ }
99
+ return health;
89
100
  }
90
101
 
91
102
  export function emptyWorkflowState() {
@@ -12,8 +12,8 @@ export function skillsWithoutWorkflow(workspace) {
12
12
  return sortSkillGroups(groups);
13
13
  }
14
14
 
15
- export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units: [] }) {
16
- if (workflowName === null) {
15
+ export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units: [] }, agentName) {
16
+ if (workflowName === null && workspace.version !== 2) {
17
17
  const groups = emptySkillGroups();
18
18
  groups.installed_only = installedOnlyEntries(workspace);
19
19
  return sortSkillGroups(groups);
@@ -24,7 +24,7 @@ export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units
24
24
  for (const summary of listSkills(workspace)) {
25
25
  const skill = workspace.skills.find((candidate) => candidate.id === summary.id);
26
26
  const explanation = explainSkill(workspace, summary.id);
27
- const use = canUseSkill(workspace, summary.id, workflowName);
27
+ const use = canUseSkill(workspace, summary.id, workflowName, agentName);
28
28
  const group = groupForDeclaredSkill(workspace, skill, use, sourceFindings);
29
29
  const entry = declaredSkillEntry(summary, explanation, use, group);
30
30
  if (use.allowed) {
@@ -42,6 +42,16 @@ export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units
42
42
  }
43
43
 
44
44
  function declaredSkillEntry(summary, explanation, use, group) {
45
+ if (summary.enabled !== undefined) {
46
+ return {
47
+ id: summary.id, label: summary.id, path: summary.path,
48
+ reason: reasonForGroup(group, use?.reasons ?? []),
49
+ advanced: {
50
+ enabled: summary.enabled, shared: summary.shared, installed_on: explanation.inventory?.installed_on ?? [], preference: summary.preference,
51
+ runtime_ready: !(use?.reasons ?? []).some((reason) => reason.includes("Inventory integrity error"))
52
+ }
53
+ };
54
+ }
45
55
  return {
46
56
  id: summary.id,
47
57
  label: summary.id,
@@ -79,8 +89,9 @@ function groupForDeclaredSkill(workspace, skill, use, sourceFindings) {
79
89
 
80
90
  function installedOnlyEntries(workspace) {
81
91
  const declaredPaths = new Set(workspace.skills.map((skill) => skill.path));
92
+ const declaredIds = new Set(workspace.skills.map((skill) => skill.id));
82
93
  return workspace.installedSkills
83
- .filter((skill) => !declaredPaths.has(skill.path))
94
+ .filter((skill) => !declaredPaths.has(skill.path) && !declaredIds.has(skill.id))
84
95
  .map((skill) => ({
85
96
  id: skill.id,
86
97
  label: skill.name ?? skill.id,
@@ -151,7 +162,9 @@ function findingsBySource(sourceAudit) {
151
162
  }
152
163
 
153
164
  function isNotInWorkflowReason(reason) {
154
- return reason.includes("is not active, preferred, or fallback in workflow");
165
+ return reason.includes("is not active, preferred, or fallback in workflow")
166
+ || reason.includes("is not available in workflow")
167
+ || reason.includes("requires workflow");
155
168
  }
156
169
 
157
170
  function isReviewReason(reason) {
package/src/advisor.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./control.mjs";
5
5
  import { doctorProject } from "./doctor.mjs";
6
6
  import { routeSkill } from "./route.mjs";
7
- import { buildActionCards, buildInitActions } from "./advisor/actions.mjs";
7
+ import { buildActionCards, buildInitActions, buildV2ActionCards } from "./advisor/actions.mjs";
8
8
  import {
9
9
  buildBrief,
10
10
  buildCleanup,
@@ -25,14 +25,14 @@ import { loadWorkspace } from "./workspace.mjs";
25
25
 
26
26
  export async function buildSkillBrief(options = {}) {
27
27
  const paths = resolveProjectPaths(options);
28
- const cleanup = await buildCleanup(paths.root);
29
28
  const configDoctor = await doctorProject({
30
29
  root: paths.root,
31
30
  configPath: paths.configPath,
32
31
  skillsRoot: paths.skillsRoot
33
32
  });
34
33
 
35
- if (!configDoctor.config.exists || !configDoctor.initialized) {
34
+ if (!configDoctor.config.exists) {
35
+ const cleanup = userStateCleanup();
36
36
  return buildBrief({
37
37
  ok: false,
38
38
  error: {
@@ -50,6 +50,7 @@ export async function buildSkillBrief(options = {}) {
50
50
  }
51
51
 
52
52
  if (!configDoctor.config.valid) {
53
+ const cleanup = userStateCleanup();
53
54
  return buildExpectedConfigError(configDoctor, paths, cleanup, {
54
55
  code: "invalid-config",
55
56
  message: configDoctor.config.error ?? "skillboard.config.yaml is invalid"
@@ -60,12 +61,15 @@ export async function buildSkillBrief(options = {}) {
60
61
  try {
61
62
  workspace = await loadWorkspace({ configPath: paths.configPath, skillsRoot: paths.skillsRoot });
62
63
  } catch (error) {
64
+ const cleanup = userStateCleanup();
63
65
  return buildExpectedConfigError(configDoctor, paths, cleanup, {
64
66
  code: "invalid-config",
65
67
  message: error instanceof Error ? error.message : String(error)
66
68
  }, options);
67
69
  }
68
70
 
71
+ const cleanup = workspace.version === 2 ? userStateCleanup() : await buildCleanup(paths.root);
72
+
69
73
  const doctor = await doctorProject({
70
74
  root: paths.root,
71
75
  configPath: paths.configPath,
@@ -74,13 +78,16 @@ export async function buildSkillBrief(options = {}) {
74
78
  verifySources: options.verifySources
75
79
  });
76
80
  const sourceAudit = auditSources(workspace);
77
- const workflow = resolveWorkflow(listWorkflows(workspace), options.workflow);
78
- const reviewQueue = buildReviewQueue(workspace, sourceAudit);
81
+ const workflow = workspace.version === 2
82
+ ? emptyWorkflowState()
83
+ : resolveWorkflow(listWorkflows(workspace), options.workflow);
84
+ const reviewQueue = workspace.version === 2 ? [] : buildReviewQueue(workspace, sourceAudit);
79
85
 
80
86
  if (workflow.unknown) {
81
87
  const skills = skillsWithoutWorkflow(workspace);
82
88
  const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
83
89
  return buildBrief({
90
+ compatibility: workspace.compatibility,
84
91
  ok: false,
85
92
  error: {
86
93
  code: "unknown-workflow",
@@ -96,11 +103,13 @@ export async function buildSkillBrief(options = {}) {
96
103
  });
97
104
  }
98
105
 
99
- const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit);
106
+ const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit, options.agent);
100
107
  const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
101
108
  const route = routeForBrief({ options, paths, workflow, workspace });
102
- const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.sources.ok;
109
+ const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.inventory.ok
110
+ && (workspace.version === 2 || doctor.sources.ok);
103
111
  return buildBrief({
112
+ compatibility: workspace.compatibility,
104
113
  ok: availabilityOk,
105
114
  health: healthForBrief(doctor, paths, availabilityOk),
106
115
  workflow,
@@ -113,14 +122,22 @@ export async function buildSkillBrief(options = {}) {
113
122
  });
114
123
  }
115
124
 
125
+ function userStateCleanup() {
126
+ return {
127
+ conservative: { dryRun: true, removed: [], updated: [], preserved: [] },
128
+ full_reset: { dryRun: true, removed: [], updated: [], preserved: [] }
129
+ };
130
+ }
131
+
116
132
  function routeForBrief({ options, paths, workflow, workspace }) {
117
133
  const intent = options.intent?.trim();
118
- if (intent === undefined || intent.length === 0 || workflow.selected === null || workflow.unknown || workflow.needs_selection) {
134
+ const workflowRequired = workspace.version !== 2 && workflow.selected === null;
135
+ if (intent === undefined || intent.length === 0 || workflowRequired || workflow.unknown || workflow.needs_selection) {
119
136
  return undefined;
120
137
  }
121
138
  return routeSkill(workspace, {
122
139
  intent,
123
- workflow: workflow.selected,
140
+ ...(workspace.version === 2 ? { agent: options.agent } : workflow.selected === null ? {} : { workflow: workflow.selected }),
124
141
  configPath: paths.configPath,
125
142
  skillsRoot: paths.skillsRoot
126
143
  });
@@ -155,6 +172,7 @@ function actionsForBrief(context) {
155
172
  if (!requestedActions(context.options)) {
156
173
  return { reviewQueue: context.reviewQueue, actions: undefined };
157
174
  }
175
+ if (context.workspace.version === 2) return { reviewQueue: [], actions: buildV2ActionCards(context) };
158
176
  return buildActionCards(context);
159
177
  }
160
178
 
@@ -1,16 +1,30 @@
1
1
  import { isAbsolute, relative, resolve } from "node:path";
2
+ import { mkdir } from "node:fs/promises";
2
3
  import { createInterface } from "node:readline";
3
4
  import { installAgentIntegration, uninstallAgentIntegration } from "./agent-integration-files.mjs";
4
- import { resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
5
+ import { applyOwnership, applyOwnershipTree, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
5
6
  import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
7
+ import {
8
+ agentRootRegistryPath,
9
+ loadRegisteredAgentRoots,
10
+ mergeRegisteredAgentRoots,
11
+ proposedAgentRoot,
12
+ writeRegisteredAgentRoots
13
+ } from "./agent-root-registry.mjs";
14
+ import { refreshAgentInventory } from "./inventory-refresh.mjs";
15
+ import { reconcileSharedSkills } from "./shared-skill-reconcile.mjs";
6
16
 
7
17
  export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
18
+ assertSetupOptions(options);
8
19
  if (options.get("dir") !== undefined) {
9
20
  throw new Error("skillboard setup is agent-layer setup and does not accept --dir");
10
21
  }
11
22
  const env = runtime.env ?? process.env;
12
23
  const home = await resolveSetupHome(env, runtime);
13
- const targets = await agentSetupTargets(options, runtime, home);
24
+ const existingRoots = await loadRegisteredAgentRoots(home);
25
+ const proposedRoot = await setupSkillRoot(options, home, runtime.cwd ?? process.cwd());
26
+ const registeredRoots = mergeRegisteredAgentRoots(existingRoots, proposedRoot);
27
+ const targets = await agentSetupTargets(options, runtime, home, registeredRoots);
14
28
  if (targets.length === 0) {
15
29
  stdout.write("No supported agent user skill roots were detected.\n");
16
30
  stdout.write(`Create a supported agent home or pass --agent ${supportedAgentNames().join(",")} to choose targets.\n`);
@@ -27,15 +41,53 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
27
41
  return 1;
28
42
  }
29
43
  }
30
- const result = await installAgentIntegration(targets, setupOwnership(env, runtime, home));
44
+ const ownership = setupOwnership(env, runtime, home);
45
+ const result = await installAgentIntegration(targets, ownership);
46
+ await mkdir(home, { recursive: true });
47
+ let inventory = await refreshAgentInventory({
48
+ root: home,
49
+ home,
50
+ env,
51
+ registeredRoots,
52
+ preserveLegacyPolicy: true
53
+ });
54
+ const configPath = resolve(home, inventory.configPath);
55
+ const inventoryPath = inventory.inventoryPath === null ? null : resolve(home, inventory.inventoryPath);
56
+ const shared = inventoryPath === null
57
+ ? { created: [], unchanged: [], preserved: [], blocked: [] }
58
+ : await reconcileSharedSkills({ home, env, targets, configPath, inventoryPath });
59
+ if (shared.created.length > 0) {
60
+ inventory = await refreshAgentInventory({ root: home, home, env, registeredRoots });
61
+ }
62
+ for (const entry of [...shared.created, ...shared.unchanged]) {
63
+ await applyOwnershipTree(entry.path, ownership);
64
+ }
65
+ if (proposedRoot !== undefined) {
66
+ await writeRegisteredAgentRoots(home, registeredRoots);
67
+ await applyOwnership(agentRootRegistryPath(home), ownership);
68
+ }
69
+ await applyOwnership(configPath, ownership);
70
+ if (inventoryPath !== null) await applyOwnership(inventoryPath, ownership);
31
71
  stdout.write("SkillBoard agent integration installed.\n");
32
72
  writeList(stdout, "Created", result.created);
33
73
  writeList(stdout, "Updated", result.updated);
34
74
  writeList(stdout, "Unchanged", result.unchanged);
35
75
  writeList(stdout, "Preserved", result.preserved);
76
+ if (proposedRoot !== undefined) stdout.write(`Registered agent roots: ${registeredRoots.length}\n`);
77
+ if (shared.created.length > 0) stdout.write(`Created shared copies: ${shared.created.length}\n`);
78
+ if (shared.unchanged.length > 0) stdout.write(`Unchanged shared copies: ${shared.unchanged.length}\n`);
79
+ writeList(stdout, "Preserved shared copies", shared.preserved.map(formatSharedEntry));
80
+ writeList(stdout, "Blocked shared copies", shared.blocked.map(formatBlockedEntry));
81
+ stdout.write(`User policy: ${inventory.configPath}\n`);
82
+ stdout.write(`Observed skills: ${inventory.scan.scannedSkills}\n`);
83
+ if (inventory.inventoryPath === null) {
84
+ stdout.write("Policy version 1 remains read-only during SkillBoard v0.3.x.\n");
85
+ stdout.write(`Preview migration: ${commandPrefix(runtime)} migrate v2 --config ${shellQuote(configPath, runtime.platform)} --json\n`);
86
+ }
36
87
  stdout.write("Next:\n");
37
88
  stdout.write("- Restart or refresh agents that cache user skills.\n");
38
- stdout.write("- No project was initialized; skillboard init is deprecated project-local policy bootstrap and is not needed for normal use.\n");
89
+ stdout.write("- Run skillboard doctor --summary to check policy and executable paths.\n");
90
+ stdout.write("- User-level policy and inventory were refreshed; no project was initialized.\n");
39
91
  stdout.write('- Ask the agent in a workspace: "Review this plan and point out weak assumptions."\n');
40
92
  stdout.write("- SkillBoard will step in when skills overlap, routing is ambiguous, or you ask for a skill decision.\n");
41
93
  return 0;
@@ -76,7 +128,7 @@ function writeList(stdout, label, values) {
76
128
  function writeSetupConfirmation(stdout, targets, command) {
77
129
  stdout.write("SkillBoard setup installs agent-layer integration, not project files.\n");
78
130
  stdout.write("It writes a SkillBoard guidance skill into detected user agent skill roots so agents can resolve skill priority when choices overlap.\n");
79
- stdout.write("It does not create skillboard.config.yaml or .skillboard/; skillboard init is deprecated project-local policy bootstrap and is not needed for normal use.\n");
131
+ stdout.write("It creates ~/skillboard.config.yaml and ~/.skillboard/inventory.json as one user-level control plane; skillboard init is not needed for normal use.\n");
80
132
  stdout.write("Targets:\n");
81
133
  for (const target of targets) {
82
134
  stdout.write(`- ${target.agent}: ${target.skillPath}\n`);
@@ -127,7 +179,7 @@ function defaultRuntime() {
127
179
  };
128
180
  }
129
181
 
130
- async function agentSetupTargets(options, runtime, setupHome) {
182
+ async function agentSetupTargets(options, runtime, setupHome, registeredRoots) {
131
183
  const env = runtime.env ?? process.env;
132
184
  const home = setupHome ?? await resolveSetupHome(env, runtime);
133
185
  const requested = readCsv(options.get("agent"));
@@ -138,20 +190,48 @@ async function agentSetupTargets(options, runtime, setupHome) {
138
190
  if (!supported.has(name)) {
139
191
  throw new Error(`Unsupported setup agent: ${name}`);
140
192
  }
141
- targets.push(...await setupAgentSkillTargets(name, home, env, { includeFallback: requested.length > 0 }));
193
+ targets.push(...await setupAgentSkillTargets(name, home, env, {
194
+ includeFallback: requested.length > 0,
195
+ registeredRoots
196
+ }));
142
197
  }
143
198
  return targets;
144
199
  }
145
200
 
201
+ async function setupSkillRoot(options, home, cwd) {
202
+ const skillRoot = options.get("skill-root");
203
+ if (skillRoot === undefined) return undefined;
204
+ const requested = readCsv(options.get("agent"));
205
+ if (requested.length !== 1) {
206
+ throw new Error("--skill-root requires exactly one --agent value.");
207
+ }
208
+ return await proposedAgentRoot(home, requested[0], skillRoot, cwd);
209
+ }
210
+
211
+ function assertSetupOptions(options) {
212
+ const allowed = new Set(["yes", "agent", "skill-root"]);
213
+ for (const option of options.keys()) {
214
+ if (!allowed.has(option)) throw new Error(`Unknown setup option: --${option}`);
215
+ }
216
+ }
217
+
218
+ function formatSharedEntry(entry) {
219
+ return `${entry.agent}:${entry.skill}:${entry.path}`;
220
+ }
221
+
222
+ function formatBlockedEntry(entry) {
223
+ return `${entry.agent ?? "unknown"}:${entry.skill}:${entry.reason}`;
224
+ }
225
+
146
226
  function commandPrefix(runtime) {
147
227
  const entrypoint = runtime.entrypointPath ?? "";
148
228
  const normalized = entrypoint.replace(/\\/g, "/");
149
229
  if (normalized.includes("/_npx/")) {
150
230
  const packageSpec = runtime.packageSpec ?? "agent-skillboard";
151
- return `npx --yes --package ${shellQuote(packageSpec)} skillboard`;
231
+ return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
152
232
  }
153
233
  if (isSourceTreeEntrypoint(entrypoint)) {
154
- return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd())}`;
234
+ return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
155
235
  }
156
236
  return "skillboard";
157
237
  }
@@ -167,18 +247,21 @@ function isSourceTreeEntrypoint(entrypoint) {
167
247
  && !normalized.includes("/.npm/");
168
248
  }
169
249
 
170
- function sourceTreeEntrypoint(entrypoint, cwd) {
250
+ function sourceTreeEntrypoint(entrypoint, cwd, platform) {
171
251
  const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
172
252
  const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
173
253
  if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
174
- return shellQuote(relativeEntrypoint);
254
+ return shellQuote(relativeEntrypoint, platform);
175
255
  }
176
- return shellQuote(absoluteEntrypoint);
256
+ return shellQuote(absoluteEntrypoint, platform);
177
257
  }
178
258
 
179
- function shellQuote(value) {
259
+ function shellQuote(value, platform = process.platform) {
180
260
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
181
261
  return value;
182
262
  }
263
+ if (platform === "win32") {
264
+ return `"${value.replace(/"/g, '""')}"`;
265
+ }
183
266
  return `'${value.replace(/'/g, "'\\''")}'`;
184
267
  }
@@ -1,10 +1,10 @@
1
1
  export const AGENT_INTEGRATION_START = "<!-- BEGIN SKILLBOARD AGENT INTEGRATION -->";
2
2
  export const AGENT_INTEGRATION_END = "<!-- END SKILLBOARD AGENT INTEGRATION -->";
3
3
 
4
- export function agentIntegrationSkill() {
4
+ export function agentIntegrationSkill(agent = "<current-agent>") {
5
5
  return `---
6
6
  name: skillboard
7
- description: Use SkillBoard when several installed skills could apply, a skill choice is ambiguous, workflow priority matters, or the user explicitly asks which skill should be used, imported, preferred, avoided, reviewed, or prioritized.
7
+ description: Use SkillBoard when several installed skills could apply, a skill choice is ambiguous, or the user explicitly asks which skill should be used, shared, imported, preferred, avoided, or prioritized.
8
8
  ---
9
9
  ${AGENT_INTEGRATION_START}
10
10
  # SkillBoard Agent Integration
@@ -13,21 +13,24 @@ Use this skill to let SkillBoard guide skill selection above individual projects
13
13
 
14
14
  ## Layering
15
15
 
16
- - SkillBoard is the user-level control plane for skill priority, overlap resolution, and workflow-aware routing.
16
+ - SkillBoard is the user-level control plane for skill priority, overlap resolution, and opt-in cross-agent sharing.
17
17
  - Project management belongs to the agent or workspace layer. Do not initialize, attach, rewrite, or manage a project just because SkillBoard is installed.
18
- - Package install and \`skillboard setup\` install user-agent guidance only.
18
+ - Package install and \`skillboard setup\` refresh user-agent guidance plus the home policy and inventory.
19
19
  - \`skillboard init\` is deprecated project-local policy bootstrap. It is not needed for normal use; use it only when maintaining an existing workspace that intentionally keeps local policy files such as \`skillboard.config.yaml\`, \`.skillboard/\`, \`AGENTS.md\`, or \`CLAUDE.md\`.
20
20
 
21
21
  ## Default Behavior
22
22
 
23
- - Installed user skills are usable by default unless the runtime, user, or local instructions disable them.
24
- - For ordinary user requests, work normally; invoke SkillBoard only when skill choice is ambiguous, skills overlap, workflow priority matters, or the user asks for a SkillBoard or skill decision.
23
+ - Installed skills stay agent-local by default. Only a skill the user explicitly shares is promoted across supported agents.
24
+ - This integration is running for agent \`${agent}\`; pass \`--agent ${agent}\` to brief, route, can-use, and guard.
25
+ - For ordinary user requests, work normally; invoke SkillBoard only when skill choice is ambiguous, skills overlap, or the user asks for a SkillBoard or skill decision.
25
26
  - If the user explicitly asks for a specific installed skill, honor that request when the guard allows it instead of rerouting away solely because other skills also match.
26
27
  - Do not ask for permission merely because you selected a skill.
27
28
  - When you use a skill, disclose it briefly at the start and completion. If SkillBoard says remembered or configured policy selected this skill while other allowed skills were available, mention that at completion.
28
29
 
29
30
  ## Cross-Agent Skill Reuse
30
31
 
32
+ - When the user wants one installed skill available to all supported agents, confirm that persistent policy change once and run \`skillboard skill share <skill> --json\`.
33
+ - To stop SkillBoard-managed propagation, confirm once and run \`skillboard skill unshare <skill> --json\`. This preserves agent-owned originals and removes only SkillBoard-managed copies.
31
34
  - When the user wants to use a skill from another agent, run \`skillboard import-skill --from <source-agent> --to <this-agent> --skill <skill> --json\`.
32
35
  - If SkillBoard reports the skill is compatible, install it with \`--yes\` and use the copied target-agent skill.
33
36
  - If SkillBoard reports \`needs-adaptation\`, explain the compatibility reasons and ask before changing the skill body for this agent.
@@ -37,11 +40,18 @@ Use this skill to let SkillBoard guide skill selection above individual projects
37
40
  ## Ambiguity Resolution
38
41
 
39
42
  1. Start from the user's task, not from a pre-task inventory prompt.
40
- 2. Identify candidate skills only when multiple installed skills plausibly match, a manual skill-control request is present, or workflow priority could change the route.
41
- 3. Prefer the skill whose explicit request, description, workflow guidance, and local instructions most directly match the user's task.
42
- 4. If a project or agent has explicit SkillBoard policy, use \`skillboard brief --intent <request> --json\` or \`skillboard route <intent> --workflow <name> --json\` to break ties.
43
- 5. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
44
- 6. Continue with the selected skill; do not stop only because other candidate skills exist.
43
+ 2. Identify candidate skills only when multiple installed skills plausibly match or a manual skill-control request is present.
44
+ 3. Prefer the skill whose explicit request, description, and local instructions most directly match the user's task.
45
+ 4. Use \`skillboard brief --agent ${agent} --intent <request> --json\` or \`skillboard route <intent> --agent ${agent} --json\` to break ties.
46
+ 5. Run \`skillboard guard use <skill-id> --agent ${agent} --json\` immediately before invoking the selected skill.
47
+ 6. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
48
+ 7. Continue with the selected skill; do not stop only because other candidate skills exist.
49
+
50
+ ## Removal
51
+
52
+ - If an owning installer removed a skill and inventory refresh reports stale policy, confirm once before running \`skillboard skill forget <skill-id>\`. Forget never deletes skill files and refuses installed or shared skills.
53
+ - When the user wants to remove SkillBoard itself, run \`skillboard uninstall --user --dry-run\`, show the managed cleanup plan, confirm once, then run \`skillboard uninstall --user --yes\`. Package removal comes afterward.
54
+ - User cleanup preserves agent-owned and unmanaged skills while removing marker-owned shared copies, managed guidance, and SkillBoard home state.
45
55
 
46
56
  ${AGENT_INTEGRATION_END}
47
57
  `;
@@ -8,8 +8,8 @@ export async function installAgentIntegration(targets, ownership = null) {
8
8
  const updated = [];
9
9
  const unchanged = [];
10
10
  const preserved = [];
11
- const content = agentIntegrationSkill();
12
11
  for (const target of targets) {
12
+ const content = agentIntegrationSkill(target.agent);
13
13
  if (!await isSafeManagedSkillTarget(target)) {
14
14
  preserved.push(`${target.agent}:${target.skillPath}`);
15
15
  continue;
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, chown, lstat, readFile } from "node:fs/promises";
2
+ import { access, chown, lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, relative, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
@@ -53,6 +53,19 @@ export async function applyOwnership(path, ownership) {
53
53
  }
54
54
  }
55
55
 
56
+ export async function applyOwnershipTree(path, ownership) {
57
+ if (ownership === null) return;
58
+ const stats = await pathStats(path);
59
+ if (stats === null || stats.isSymbolicLink()) return;
60
+ await applyOwnership(path, ownership);
61
+ if (!stats.isDirectory()) return;
62
+ const entries = await readdir(path, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ if (entry.isSymbolicLink()) continue;
65
+ await applyOwnershipTree(resolve(path, entry.name), ownership);
66
+ }
67
+ }
68
+
56
69
  function canApplyProcessOwnership(uid, gid) {
57
70
  if (typeof process.getuid !== "function") {
58
71
  return false;