agent-skillboard 0.2.17 → 0.3.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 (77) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +128 -260
  3. package/bin/postinstall.mjs +2 -2
  4. package/docs/adapters.md +37 -113
  5. package/docs/ai-skill-routing-goal.md +35 -109
  6. package/docs/capabilities.md +17 -104
  7. package/docs/install.md +39 -493
  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 +117 -356
  12. package/docs/rollout-runbook.md +23 -25
  13. package/docs/routing.md +23 -90
  14. package/docs/user-flow.md +60 -292
  15. package/docs/value-proof.md +23 -181
  16. package/docs/variant-lifecycle.md +47 -67
  17. package/docs/versioning.md +31 -264
  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 +13 -4
  29. package/src/agent-integration-content.mjs +22 -12
  30. package/src/agent-integration-files.mjs +1 -1
  31. package/src/agent-inventory-platforms.mjs +10 -0
  32. package/src/agent-inventory.mjs +23 -1
  33. package/src/agent-skill-import.mjs +2 -2
  34. package/src/audit-paths.mjs +42 -0
  35. package/src/brief-cli.mjs +3 -2
  36. package/src/brief-renderer.mjs +1 -0
  37. package/src/cli.mjs +398 -127
  38. package/src/compatibility.mjs +24 -0
  39. package/src/control/can-use-guard.mjs +21 -1
  40. package/src/control/config-write.mjs +32 -2
  41. package/src/control/skill-crud.mjs +5 -0
  42. package/src/control/v2-guard.mjs +175 -0
  43. package/src/control/v2-skill-crud.mjs +32 -0
  44. package/src/control/v2-skill-forget.mjs +38 -0
  45. package/src/control/variant-status.mjs +47 -1
  46. package/src/control.mjs +55 -0
  47. package/src/doctor.mjs +65 -6
  48. package/src/domain/v2-policy.mjs +111 -0
  49. package/src/hook-plan.mjs +33 -3
  50. package/src/impact.mjs +52 -29
  51. package/src/index.mjs +25 -1
  52. package/src/init.mjs +50 -34
  53. package/src/inventory-install-units.mjs +63 -0
  54. package/src/inventory-json.mjs +279 -0
  55. package/src/inventory-refresh.mjs +163 -18
  56. package/src/lifecycle-cli.mjs +40 -12
  57. package/src/lifecycle-content.mjs +52 -67
  58. package/src/migration/v1-to-v2.mjs +212 -0
  59. package/src/migration/v2-files.mjs +211 -0
  60. package/src/migration/v2-journal.mjs +169 -0
  61. package/src/migration/v2-projection.mjs +108 -0
  62. package/src/migration/v2-transaction.mjs +205 -0
  63. package/src/policy.mjs +3 -0
  64. package/src/reconcile.mjs +139 -111
  65. package/src/report.mjs +168 -148
  66. package/src/review.mjs +2 -0
  67. package/src/route-advisory.mjs +47 -2
  68. package/src/route-selection.mjs +38 -2
  69. package/src/route.mjs +62 -2
  70. package/src/shared-skill.mjs +301 -0
  71. package/src/source-digest.mjs +42 -0
  72. package/src/source-profiles.mjs +27 -0
  73. package/src/source-verification.mjs +32 -48
  74. package/src/uninstall.mjs +22 -0
  75. package/src/user-state-paths.mjs +19 -0
  76. package/src/user-uninstall.mjs +146 -0
  77. package/src/workspace.mjs +41 -1
@@ -0,0 +1,35 @@
1
+ version: 2
2
+
3
+ skills:
4
+ anthropic.docx:
5
+ enabled: true
6
+ shared: true
7
+
8
+ omo.review-work:
9
+ enabled: true
10
+ shared: false
11
+ preference:
12
+ intents:
13
+ - review
14
+ priority: 90
15
+
16
+ private.tdd-work-continuity:
17
+ enabled: true
18
+ shared: false
19
+ preference:
20
+ intents:
21
+ - implementation
22
+ - testing
23
+ priority: 100
24
+
25
+ private.workflow-router:
26
+ enabled: true
27
+ shared: false
28
+ preference:
29
+ intents:
30
+ - routing
31
+ priority: 100
32
+
33
+ unused.example:
34
+ enabled: false
35
+ shared: false
@@ -0,0 +1,6 @@
1
+ version: 2
2
+
3
+ skills:
4
+ invalid-sharing-value:
5
+ enabled: true
6
+ shared: all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.2.17",
3
+ "version": "0.3.0",
4
4
  "description": "Keep agent skills broadly available while routing overlaps consistently.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -17,17 +17,17 @@ const NON_ACTIVATABLE_STATUSES = new Set(["blocked", "deprecated", "archived", "
17
17
 
18
18
  export function buildInitActions(paths) {
19
19
  return [makeAction({
20
- kind: "init-project",
20
+ kind: "setup-user",
21
21
  targetId: paths.root,
22
- label: "Initialize SkillBoard in this project",
23
- reason: "SkillBoard is not initialized for this project.",
22
+ label: "Set up the SkillBoard user control plane",
23
+ reason: "The user-level SkillBoard policy and generated inventory do not exist yet.",
24
24
  risk: "low",
25
25
  requiresUserConfirmation: true,
26
26
  dryRun: null,
27
27
  apply: null,
28
- appliesTo: { kind: "project", id: paths.root },
29
- blockedReason: "skillboard init does not have a dry-run preview command.",
30
- advanced: { root: paths.root }
28
+ appliesTo: { kind: "user_state", id: paths.root },
29
+ blockedReason: "Run `skillboard setup --yes` after user confirmation.",
30
+ advanced: { root: paths.root, command: "skillboard setup --yes" }
31
31
  })];
32
32
  }
33
33
 
@@ -47,6 +47,102 @@ export function buildActionCards(context) {
47
47
  };
48
48
  }
49
49
 
50
+ export function buildV2ActionCards({ options, paths, workflow, workspace }) {
51
+ if (workflow.unknown || workflow.needs_selection) {
52
+ return [];
53
+ }
54
+ const actions = [];
55
+ const observedSkills = new Set(workspace.inventory.skillIds);
56
+ for (const skill of workspace.skills) {
57
+ if (!observedSkills.has(skill.id)) {
58
+ actions.push(skill.shared
59
+ ? v2SharingAction(skill, paths, "unshare")
60
+ : v2ForgetAction(skill, paths));
61
+ continue;
62
+ }
63
+ const kind = skill.enabled ? "v2:disable-skill" : "v2:enable-skill";
64
+ const operation = skill.enabled ? "disable" : "enable";
65
+ const policyArgs = ["skillboard", "skill", operation, skill.id, "--config", paths.configPath,
66
+ ...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
67
+ const dryRun = command([...policyArgs, "--dry-run", "--json"]);
68
+ actions.push(makeAction({
69
+ kind,
70
+ targetId: skill.id,
71
+ label: `${skill.enabled ? "Disable" : "Enable"} ${skill.id}`,
72
+ reason: `${skill.enabled ? "Stop" : "Allow"} SkillBoard from selecting this skill.`,
73
+ risk: "medium",
74
+ dryRun,
75
+ apply: command([...policyArgs, "--json"]),
76
+ appliesTo: { kind: "skill", id: skill.id },
77
+ blockedReason: null,
78
+ advanced: { enabled: !skill.enabled, policy_projection_version: 2 }
79
+ }));
80
+ actions.push(v2SharingAction(skill, paths, skill.shared ? "unshare" : "share"));
81
+ const intent = options?.intent?.trim();
82
+ if (intent !== undefined && intent.length > 0
83
+ && skill.enabled
84
+ && !samePreference(skill.preference, intent)) {
85
+ const intents = [...new Set([...(skill.preference?.intents ?? []), intent])]
86
+ .sort((left, right) => left.localeCompare(right));
87
+ const preferenceArgs = ["skillboard", "skill", "preference", skill.id, "--intent", intents.join(","), "--priority", "100", "--config", paths.configPath,
88
+ ...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
89
+ actions.push(makeAction({
90
+ kind: "v2:prefer-skill",
91
+ targetId: `${skill.id}:${encodeURIComponent(intent)}`,
92
+ label: `Prefer ${skill.id} for ${intent}`,
93
+ reason: "Rank this enabled skill higher when it is installed for the current agent and the intent matches.",
94
+ risk: "medium",
95
+ dryRun: command([...preferenceArgs, "--dry-run", "--json"]),
96
+ apply: command([...preferenceArgs, "--json"]),
97
+ appliesTo: { kind: "skill", id: skill.id },
98
+ blockedReason: null,
99
+ advanced: { intents, priority: 100, policy_projection_version: 2 }
100
+ }));
101
+ }
102
+ }
103
+ return withApplicationCommands(actions.sort(sortActions), { options, paths, workflow, workspace });
104
+ }
105
+
106
+ function v2SharingAction(skill, paths, operation) {
107
+ const args = ["skillboard", "skill", operation, skill.id, "--config", paths.configPath];
108
+ const unshare = operation === "unshare";
109
+ return makeAction({
110
+ kind: unshare ? "v2:unshare-skill" : "v2:share-skill",
111
+ targetId: skill.id,
112
+ label: `${unshare ? "Stop sharing" : "Share"} ${skill.id}`,
113
+ reason: unshare
114
+ ? "Stop SkillBoard-managed cross-agent sharing while preserving agent-owned originals."
115
+ : "Make this skill available through the user shared-skill layer.",
116
+ risk: "medium",
117
+ dryRun: command([...args, "--dry-run", "--json"]),
118
+ apply: command([...args, "--json"]),
119
+ appliesTo: { kind: "skill", id: skill.id },
120
+ blockedReason: null,
121
+ advanced: { shared: !unshare, policy_projection_version: 2 }
122
+ });
123
+ }
124
+
125
+ function v2ForgetAction(skill, paths) {
126
+ const args = ["skillboard", "skill", "forget", skill.id, "--config", paths.configPath,
127
+ ...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
128
+ return makeAction({
129
+ kind: "v2:forget-skill",
130
+ targetId: skill.id,
131
+ label: `Forget removed skill ${skill.id}`,
132
+ reason: "Remove stale SkillBoard policy after the owning installer removed this unshared skill.",
133
+ risk: "medium",
134
+ dryRun: command([...args, "--dry-run", "--json"]),
135
+ apply: command([...args, "--json"]),
136
+ appliesTo: { kind: "skill", id: skill.id },
137
+ blockedReason: null,
138
+ advanced: { policy_only: true, policy_projection_version: 2 }
139
+ });
140
+ }
141
+
142
+ function samePreference(preference, intent) {
143
+ return preference?.priority === 100 && preference.intents.includes(intent);
144
+ }
145
+
50
146
  function reviewInstallUnitActions({ paths, workflow, reviewQueue }) {
51
147
  const units = new Map();
52
148
  for (const entry of reviewQueue) {
@@ -3,17 +3,17 @@ import {
3
3
  workflowResolved
4
4
  } from "./action-core.mjs";
5
5
 
6
- export function withApplicationCommands(actions, { paths, workflow }) {
6
+ export function withApplicationCommands(actions, { paths, workflow, workspace, options }) {
7
7
  return actions.map((action) => {
8
8
  return {
9
9
  ...action,
10
- application: applicationCommand(action, paths, workflow)
10
+ application: applicationCommand(action, paths, workflow, workspace, options)
11
11
  };
12
12
  });
13
13
  }
14
14
 
15
- function applicationCommand(action, paths, workflow) {
16
- const blockedReason = applicationBlockedReason(action, workflow);
15
+ function applicationCommand(action, paths, workflow, workspace, options) {
16
+ const blockedReason = applicationBlockedReason(action, workflow, workspace);
17
17
  if (blockedReason !== null) {
18
18
  return {
19
19
  preview: null,
@@ -24,11 +24,12 @@ function applicationCommand(action, paths, workflow) {
24
24
 
25
25
  const base = [
26
26
  "skillboard", "apply-action", action.id,
27
- ...workflowArgs(workflow),
27
+ ...(workspace.version === 2 ? [] : workflowArgs(workflow)),
28
28
  "--dir", paths.root,
29
29
  "--config", paths.configPath,
30
- "--skills", paths.skillsRoot,
31
- "--json"
30
+ ...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot]),
31
+ "--json",
32
+ ...(workspace.version === 2 && options?.agent !== undefined ? ["--agent", options.agent] : [])
32
33
  ];
33
34
  return {
34
35
  preview: command([...base, "--dry-run"]),
@@ -37,14 +38,14 @@ function applicationCommand(action, paths, workflow) {
37
38
  };
38
39
  }
39
40
 
40
- function applicationBlockedReason(action, workflow) {
41
+ function applicationBlockedReason(action, workflow, workspace) {
41
42
  if (action.blocked_reason !== null) {
42
43
  return action.blocked_reason;
43
44
  }
44
45
  if (action.apply === null) {
45
46
  return "Action cannot be applied directly.";
46
47
  }
47
- if (!workflowResolved(workflow)) {
48
+ if (workspace.version !== 2 && !workflowResolved(workflow)) {
48
49
  return workflow.blocked_reason ?? "Select a workflow before applying action cards.";
49
50
  }
50
51
  return null;
@@ -5,8 +5,15 @@ import {
5
5
  installGuardHook,
6
6
  removeSkill
7
7
  } from "../control.mjs";
8
+ import { setV2SkillEnabled, setV2SkillPreference } from "../control/v2-skill-crud.mjs";
9
+ import { forgetV2Skill } from "../control/v2-skill-forget.mjs";
10
+ import { setSkillSharing } from "../shared-skill.mjs";
11
+ import { resolveUserStatePaths } from "../user-state-paths.mjs";
8
12
  import { reviewInstallUnit } from "../review.mjs";
9
13
  import { uninstallProject } from "../uninstall.mjs";
14
+ import { isPreV2ActionId, V1_MUTATION_ERROR } from "../compatibility.mjs";
15
+ import { loadWorkspace } from "../workspace.mjs";
16
+ import { resolveProjectPaths } from "./schema.mjs";
10
17
 
11
18
  const INSTALL_UNIT_ACTIONS = new Set([
12
19
  "block-install-unit",
@@ -27,17 +34,30 @@ export async function applyAdvisorAction(actionId, options) {
27
34
  throw new ApplyActionError("missing-action-id", "Usage: skillboard apply-action <action-id>");
28
35
  }
29
36
 
37
+ const paths = resolveProjectPaths(options);
38
+ options = { ...options, root: paths.root, configPath: paths.configPath, skillsRoot: paths.skillsRoot };
39
+ const workspace = await loadWorkspace({ configPath: options.configPath, skillsRoot: options.skillsRoot });
40
+ if (workspace.version === 1 && options.yes === true && options.dryRun !== true) {
41
+ throw new ApplyActionError("migration-required", V1_MUTATION_ERROR);
42
+ }
43
+ if (workspace.version === 2 && isPreV2ActionId(actionId)) {
44
+ throw new ApplyActionError("stale-policy-version", `Pre-v2 action id is stale: ${actionId}. Run skillboard brief --include-actions to get current actions.`);
45
+ }
30
46
  const action = await resolveCurrentAction(actionId, options);
31
47
  if (action.blocked_reason !== null) {
32
48
  throw new ApplyActionError("blocked-action", action.blocked_reason);
33
49
  }
34
50
 
35
51
  if (previewMode(options)) {
52
+ const control = action.kind.startsWith("v2:")
53
+ ? await dispatchAction(action, { ...options, dryRun: true })
54
+ : null;
36
55
  return {
37
56
  ok: true,
38
57
  mode: "preview",
39
58
  changed: false,
40
- action
59
+ action,
60
+ control
41
61
  };
42
62
  }
43
63
 
@@ -96,6 +116,8 @@ async function resolveCurrentAction(actionId, options) {
96
116
  function buildActionBrief(options) {
97
117
  return buildSkillBrief({
98
118
  includeActions: true,
119
+ agent: options.agent,
120
+ intent: options.intent,
99
121
  workflow: options.workflow,
100
122
  configPath: options.configPath,
101
123
  skillsRoot: options.skillsRoot,
@@ -108,6 +130,57 @@ function previewMode(options) {
108
130
  }
109
131
 
110
132
  async function dispatchAction(action, options) {
133
+ if (action.kind === "v2:enable-skill" || action.kind === "v2:disable-skill") {
134
+ return await setV2SkillEnabled({
135
+ skillId: action.applies_to.id,
136
+ enabled: action.kind === "v2:enable-skill",
137
+ configPath: options.configPath,
138
+ skillsRoot: options.skillsRoot,
139
+ dryRun: options.dryRun === true
140
+ });
141
+ }
142
+ if (action.kind === "v2:share-skill" || action.kind === "v2:unshare-skill") {
143
+ const state = resolveUserStatePaths({
144
+ home: options.home,
145
+ env: options.env ?? process.env,
146
+ configPath: options.configPath,
147
+ inventoryPath: options.inventoryPath
148
+ });
149
+ return await setSkillSharing({
150
+ skillId: action.applies_to.id,
151
+ shared: action.kind === "v2:share-skill",
152
+ configPath: options.configPath,
153
+ inventoryPath: state.inventoryPath,
154
+ home: state.home,
155
+ env: options.env ?? process.env,
156
+ dryRun: options.dryRun === true
157
+ });
158
+ }
159
+ if (action.kind === "v2:prefer-skill") {
160
+ return await setV2SkillPreference({
161
+ skillId: action.applies_to.id,
162
+ intents: action.advanced.intents,
163
+ priority: action.advanced.priority,
164
+ configPath: options.configPath,
165
+ skillsRoot: options.skillsRoot,
166
+ dryRun: options.dryRun === true
167
+ });
168
+ }
169
+ if (action.kind === "v2:forget-skill") {
170
+ const state = resolveUserStatePaths({
171
+ home: options.home,
172
+ env: options.env ?? process.env,
173
+ configPath: options.configPath,
174
+ inventoryPath: options.inventoryPath
175
+ });
176
+ return await forgetV2Skill({
177
+ skillId: action.applies_to.id,
178
+ configPath: options.configPath,
179
+ inventoryPath: state.inventoryPath,
180
+ skillsRoot: options.skillsRoot,
181
+ dryRun: options.dryRun === true
182
+ });
183
+ }
111
184
  if (INSTALL_UNIT_ACTIONS.has(action.kind)) {
112
185
  return await reviewInstallUnit({
113
186
  unitId: action.applies_to.id,
@@ -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) {