agent-skillboard 0.3.3 → 0.3.4

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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.4 — 2026-07-21
6
+
7
+ ### Fixed
8
+
9
+ - Separated generated-inventory integrity failures from sorted stale
10
+ removed-skill policy observations. Valid inventory now keeps doctor, status,
11
+ brief, strict health, and observed-skill routing healthy while absent skills
12
+ remain guard-denied and available for explicit, confirmation-required policy
13
+ forget actions.
14
+
5
15
  ## 0.3.3 — 2026-07-15
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -10,7 +10,7 @@ skill. The model makes semantic skill choices from the request, eligible skill
10
10
  descriptions, and raw saved preferences. SkillBoard does not tokenize, score,
11
11
  match, or recommend from v2 request text.
12
12
 
13
- Status: public alpha. Package 0.3.3 writes policy schema v2.
13
+ Status: public alpha. Package 0.3.4 writes policy schema v2.
14
14
 
15
15
  <p align="center">
16
16
  <img src="https://raw.githubusercontent.com/NyXXiR/skillboard/main/skillboard.png" alt="SkillBoard architecture diagram" width="100%">
@@ -192,6 +192,15 @@ skillboard skill forget <skill-id>
192
192
  `forget` never deletes skill files. It refuses skills that are still observed or
193
193
  shared, so shared skills must be unshared first.
194
194
 
195
+ A valid generated inventory remains healthy when it no longer observes an
196
+ unshared policy entry. Doctor/status expose sorted entries as
197
+ `inventory.stalePolicySkills`; brief exposes the same observation as
198
+ `health.inventory.stale_policy_skills`. Stale removed-skill policy does not fail
199
+ strict health, change passed mode, or block other observed skills. The absent
200
+ skill itself remains unavailable to route and guard. SkillBoard never deletes
201
+ these entries automatically: review one current forget action, confirm it, and
202
+ then reread the returned brief.
203
+
195
204
  Before removing the SkillBoard package, preview and apply the complete
196
205
  user-level cleanup:
197
206
 
@@ -54,6 +54,15 @@ optional audit metadata and never determine availability. Runtime and action
54
54
  authorization are outside SkillBoard's scope and remain with the agent or
55
55
  harness.
56
56
 
57
+ Generated-inventory integrity is separate from stale removed-skill policy.
58
+ Missing, malformed, unsupported, duplicate, or internally inconsistent
59
+ inventory remains a hard health failure. When inventory is valid, policy ids it
60
+ does not observe are reported as sorted stale-policy observations without
61
+ failing strict health, changing passed mode, or blocking other observed skills.
62
+ The absent skill remains unavailable to route and guard. Stale policy is retained
63
+ until an explicit, confirmation-required forget action removes only that policy
64
+ entry; SkillBoard never auto-deletes it.
65
+
57
66
  ## Installation default
58
67
 
59
68
  A newly discovered valid skill receives `enabled: true` and `shared: false`.
package/docs/reference.md CHANGED
@@ -103,6 +103,17 @@ provenance, digests, aliases, install units, and runtime-component observations.
103
103
  Only `installed_on` participates in the presence check. Other observations are
104
104
  optional audit metadata and never determine availability.
105
105
 
106
+ Missing, invalid JSON, unsupported flags or format, duplicate records, and
107
+ malformed records are generated-inventory integrity failures. With an otherwise
108
+ valid inventory, policy ids absent from inventory are instead sorted,
109
+ non-blocking stale-policy observations. Doctor/status return them in
110
+ `inventory.stalePolicySkills`; brief returns them in
111
+ `health.inventory.stale_policy_skills`. They do not fail strict health or stop
112
+ observed skills from routing and guard, while the specifically absent skill
113
+ remains unavailable. No command automatically removes stale policy. A current
114
+ `v2:forget-skill:<skill-id>` action requires explicit confirmation, refuses
115
+ installed or shared skills, and changes policy only.
116
+
106
117
  Runtime and action authorization are outside SkillBoard's scope. The active
107
118
  agent or harness owns permission checks for hooks, MCP servers, commands,
108
119
  network access, external writes, destructive operations, and secrets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Keep agent skills broadly available while routing overlaps consistently.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -93,7 +93,8 @@ export function healthFromDoctor(doctor, paths) {
93
93
  required: true,
94
94
  ok: doctor.inventory.ok,
95
95
  path: doctor.inventory.path,
96
- errors: sortedStrings(doctor.inventory.errors)
96
+ errors: sortedStrings(doctor.inventory.errors),
97
+ stale_policy_skills: sortedStrings(doctor.inventory.stalePolicySkills ?? [])
97
98
  };
98
99
  }
99
100
  return health;
@@ -53,6 +53,7 @@ export function renderSkillBrief(brief, options = {}) {
53
53
  }
54
54
  emitIntentRoute(lines, brief);
55
55
  emitPolicyHealth(lines, brief);
56
+ emitStalePolicy(lines, brief);
56
57
  emitCategorySummary(lines, brief);
57
58
  emitNextAction(lines, brief, { verbose });
58
59
  emitAvailableNowSection(lines, brief, { verbose });
@@ -109,6 +110,19 @@ function emitPolicyHealth(lines, brief) {
109
110
  lines.push("");
110
111
  }
111
112
 
113
+ function emitStalePolicy(lines, brief) {
114
+ const skills = brief.health?.inventory?.stale_policy_skills ?? [];
115
+ if (skills.length === 0) {
116
+ return;
117
+ }
118
+ lines.push("## Stale removed-skill policy", "");
119
+ lines.push("These policy entries are retained for explicit review and do not block other installed skills.");
120
+ for (const skill of skills) {
121
+ lines.push(`- ${safeText(skill)}`);
122
+ }
123
+ lines.push("");
124
+ }
125
+
112
126
  function emitPolicyDiagnostics(lines, label, diagnostics) {
113
127
  if (diagnostics.length === 0) {
114
128
  return;
package/src/cli.mjs CHANGED
@@ -1709,6 +1709,8 @@ function renderDoctorSummary(result) {
1709
1709
  `SkillBoard doctor: ${status}`,
1710
1710
  renderInstallation(result.installation),
1711
1711
  `Workspace: ${result.workspace.skills.declared} skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
1712
+ `Inventory integrity: ${result.inventory.ok ? "passed" : "failed"} (${result.inventory.errors.length} errors)`,
1713
+ `Stale removed-skill policy: ${result.inventory.stalePolicySkills.length}`,
1712
1714
  `Source audit: ${result.sources.ok ? "passed" : "failed"} (${result.sources.errors.length} errors, ${result.sources.warnings.length} warnings, ${result.sources.blockingWarnings.length} blocking)`,
1713
1715
  `Policy: ${result.policy.ok ? "passed" : "failed"} (${result.policy.errors.length} errors, ${result.policy.warnings.length} warnings)`
1714
1716
  ];
@@ -1746,6 +1748,8 @@ function renderDoctor(result) {
1746
1748
  `Config: ${result.config.exists ? result.config.valid ? `valid v${result.config.version}` : `invalid (${result.config.error})` : "missing"}`,
1747
1749
  `Bridge: ${bridges}`,
1748
1750
  `Workspace: ${result.workspace.skills.declared} declared skills, ${result.workspace.skills.installed} installed skills, ${result.workspace.workflows} workflows, ${result.workspace.harnesses} harnesses, ${result.workspace.installUnits.total} install units`,
1751
+ `Inventory integrity: ${result.inventory.ok ? "passed" : "failed"} (${result.inventory.errors.length} errors)`,
1752
+ `Stale removed-skill policy (${result.inventory.stalePolicySkills.length}): ${formatList(result.inventory.stalePolicySkills)}`,
1749
1753
  `Skill states: ${renderCounts(result.workspace.skills.byStatus)}`,
1750
1754
  `Invocations: ${renderCounts(result.workspace.skills.byInvocation)}`,
1751
1755
  `Source classes: ${renderCounts(result.workspace.installUnits.bySourceClass)}`,
package/src/doctor.mjs CHANGED
@@ -47,7 +47,7 @@ export async function doctorProject(options = {}) {
47
47
  bridges,
48
48
  installation,
49
49
  workspace: emptyWorkspaceSummary(),
50
- inventory: { required: false, ok: true, path: null, errors: [] },
50
+ inventory: { required: false, ok: true, path: null, errors: [], stalePolicySkills: [] },
51
51
  policy: { ok: false, errors: [], warnings: [] },
52
52
  sources: { checked: false, verified: options.verifySources === true, ok: false, errors: [], warnings: [], blockingWarnings: [], units: [] },
53
53
  uninstall,
@@ -136,12 +136,9 @@ function doctorRecommendations(result) {
136
136
  const recommendations = [];
137
137
  if (!result.inventory.ok) {
138
138
  recommendations.push("run skillboard inventory refresh and fix generated inventory integrity errors");
139
- for (const error of result.inventory.errors) {
140
- const match = /^skill (?<skill>[^ ]+) is missing from generated inventory$/u.exec(error);
141
- if (match?.groups?.skill !== undefined) {
142
- recommendations.push(`reinstall ${match.groups.skill} or run skillboard skill forget ${match.groups.skill}`);
143
- }
144
- }
139
+ }
140
+ for (const skill of result.inventory.stalePolicySkills) {
141
+ recommendations.push(`reinstall ${skill} or run skillboard skill forget ${skill}`);
145
142
  }
146
143
  if (!result.bridges.some((bridge) => bridge.status === "installed")) {
147
144
  recommendations.push("legacy project bridge blocks are absent; run skillboard init only if maintaining deprecated project-local policy");
@@ -176,20 +173,22 @@ function doctorRecommendations(result) {
176
173
 
177
174
  function inventoryHealth(workspace) {
178
175
  if (workspace.version !== 2) {
179
- return { required: false, ok: true, path: null, errors: [] };
176
+ return { required: false, ok: true, path: null, errors: [], stalePolicySkills: [] };
180
177
  }
181
178
  const errors = [...(workspace.inventory?.integrityErrors ?? ["generated inventory is unavailable"])];
182
179
  const observed = new Set(workspace.inventory?.skillIds ?? []);
183
- for (const skill of workspace.skills) {
184
- if (!observed.has(skill.id)) {
185
- errors.push(`skill ${skill.id} is missing from generated inventory`);
186
- }
187
- }
180
+ const stalePolicySkills = errors.length === 0
181
+ ? workspace.skills
182
+ .map((skill) => skill.id)
183
+ .filter((skillId) => !observed.has(skillId))
184
+ .sort((left, right) => left.localeCompare(right))
185
+ : [];
188
186
  return {
189
187
  required: true,
190
188
  ok: errors.length === 0,
191
189
  path: workspace.inventory?.path ?? null,
192
- errors
190
+ errors,
191
+ stalePolicySkills
193
192
  };
194
193
  }
195
194