agent-skillboard 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.7 — 2026-07-03
6
+
7
+ ### Fixed
8
+
9
+ - Direct `skillboard activate` and `skillboard prefer` now refuse reviewed
10
+ runtime/plugin skills that remain `status: blocked`, preserving the blocked
11
+ state outside action-card flows.
12
+
5
13
  ## 0.2.6 — 2026-07-03
6
14
 
7
15
  ### Fixed
@@ -8,7 +8,7 @@ source profiles, and the generated lockfile.
8
8
 
9
9
  ## Status
10
10
 
11
- Current package version: `0.2.6`
11
+ Current package version: `0.2.7`
12
12
 
13
13
  Current config schema version:
14
14
 
@@ -59,7 +59,7 @@ must call them out clearly.
59
59
  Suggested tags:
60
60
 
61
61
  - `v0.1.0-alpha`: first public GitHub alpha.
62
- - `v0.2.6`: one-time runtime source review before manual activation, ask-after
62
+ - `v0.2.7`: one-time runtime source review before manual activation, ask-after
63
63
  skill routing, purge uninstall, README benefit-first positioning,
64
64
  source inventory refresh, doctor/status, source pin refresh,
65
65
  installer/config detection, resilient detector warnings, and richer dry-run
@@ -194,8 +194,8 @@ Before tagging a public release:
194
194
  provenance with GitHub Actions OIDC, so keep `permissions.id-token: write`,
195
195
  the npm registry URL in `setup-node`, and
196
196
  `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` on the publish step.
197
- - Push a version tag that exactly matches `package.json`, for example `v0.2.6`
198
- for package version `0.2.6`.
197
+ - Push a version tag that exactly matches `package.json`, for example `v0.2.7`
198
+ for package version `0.2.7`.
199
199
  - Let `.github/workflows/publish.yml` publish from the tag. The workflow runs
200
200
  the full check suite, validates that the tag matches the package version, and
201
201
  skips `npm publish` only when that exact version already exists on npm.
@@ -229,6 +229,12 @@ completion notes:
229
229
  - update README quick-start to verify the installed version before running
230
230
  policy commands.
231
231
 
232
+ ## 0.2.7 Completion Notes
233
+
234
+ - reject direct `skillboard activate` and `skillboard prefer` attempts for
235
+ reviewed runtime/plugin skills that remain `status: blocked`, while
236
+ preserving reviewed quarantined manual-only activation.
237
+
232
238
  ## 0.2.6 Completion Notes
233
239
 
234
240
  - keep reviewed `status: blocked` runtime/plugin skills non-activatable while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Let AI agents pick and use allowed skills in each workflow.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,4 +1,3 @@
1
- import YAML from "yaml";
2
1
  import {
3
2
  EXPOSURE_VALUES,
4
3
  INVOCATION_VALUES,
@@ -28,6 +27,7 @@ import {
28
27
  import { canUseSkill } from "./can-use-guard.mjs";
29
28
 
30
29
  const WRITABLE_INVOCATIONS = new Set(["manual-only", "router-only", "workflow-auto", "global-auto"]);
30
+ const NON_ACTIVATABLE_STATUSES = new Set(["blocked", "deprecated", "archived", "removed"]);
31
31
 
32
32
  export async function activateSkill(options) {
33
33
  const { document, originalText } = await loadConfig(options.configPath);
@@ -38,6 +38,7 @@ export async function activateSkill(options) {
38
38
  if (!WRITABLE_INVOCATIONS.has(mode) || mode === "global-auto") {
39
39
  throw new Error(`activate requires --mode manual-only, router-only, or workflow-auto; got ${mode}`);
40
40
  }
41
+ ensureCanActivateSkill(options.skillId, skill, mode);
41
42
  skill.set("status", "active");
42
43
  skill.set("invocation", mode);
43
44
  addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
@@ -193,6 +194,7 @@ export async function preferSkill(options) {
193
194
  if (capabilityDefinition === undefined) {
194
195
  throw new Error(`Unknown capability: ${options.capability}`);
195
196
  }
197
+ ensureCanPreferSkill(options.skillId, skill);
196
198
  const required = ensureRequiredCapability(workflow, options.capability, document);
197
199
  const previousPreferred = readMapString(required, "preferred", "");
198
200
  if (previousPreferred.length > 0 && previousPreferred !== options.skillId) {
@@ -202,17 +204,6 @@ export async function preferSkill(options) {
202
204
  removeValue(ensureSeq(required, "fallback", document), options.skillId);
203
205
  addUnique(ensureSeq(workflow, "active_skills", document), options.skillId);
204
206
  removeValue(ensureSeq(workflow, "blocked_skills", document), options.skillId);
205
- const status = readMapString(skill, "status", "vendor");
206
- const invocation = readMapString(skill, "invocation", "manual-only");
207
- if (status === "quarantined" || status === "blocked") {
208
- skill.set("status", "active");
209
- }
210
- if (invocation === "blocked" || invocation === "deprecated") {
211
- const requiredPolicy = readMapString(required, "policy", "");
212
- const defaultPolicy = YAML.isMap(capabilityDefinition) ? readMapString(capabilityDefinition, "default_policy", "manual-only") : "manual-only";
213
- skill.set("invocation", requiredPolicy.length > 0 ? requiredPolicy : defaultPolicy);
214
- }
215
-
216
207
  return await writeCheckedConfig(
217
208
  document,
218
209
  originalText,
@@ -221,6 +212,31 @@ export async function preferSkill(options) {
221
212
  );
222
213
  }
223
214
 
215
+ function ensureCanActivateSkill(skillId, skill, mode) {
216
+ const status = readMapString(skill, "status", "vendor");
217
+ const invocation = readMapString(skill, "invocation", "manual-only");
218
+ if (NON_ACTIVATABLE_STATUSES.has(status)) {
219
+ throw new Error(`Cannot activate non-callable skill ${skillId} with status: ${status}`);
220
+ }
221
+ if (status === "quarantined" && mode !== "manual-only") {
222
+ throw new Error(`Cannot activate quarantined skill ${skillId} as ${mode}; use manual-only after source review`);
223
+ }
224
+ if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(invocation) && !(status === "quarantined" && invocation === "blocked" && mode === "manual-only")) {
225
+ throw new Error(`Cannot activate non-callable skill ${skillId} with invocation: ${invocation}`);
226
+ }
227
+ }
228
+
229
+ function ensureCanPreferSkill(skillId, skill) {
230
+ const status = readMapString(skill, "status", "vendor");
231
+ const invocation = readMapString(skill, "invocation", "manual-only");
232
+ if (NON_CALLABLE_WORKFLOW_STATUSES.has(status)) {
233
+ throw new Error(`Cannot prefer non-callable skill ${skillId} with status: ${status}`);
234
+ }
235
+ if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(invocation)) {
236
+ throw new Error(`Cannot prefer non-callable skill ${skillId} with invocation: ${invocation}`);
237
+ }
238
+ }
239
+
224
240
  function requireConfigSkill(document, skillId) {
225
241
  const skills = requireMapAt(document, ["skills"], "skills");
226
242
  const skill = skills.get(skillId, true);