agent-skillboard 0.1.2 → 0.2.2

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 (63) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +159 -630
  3. package/docs/adapters.md +96 -96
  4. package/docs/ai-skill-routing-goal.md +112 -0
  5. package/docs/capabilities.md +6 -0
  6. package/docs/install.md +172 -114
  7. package/docs/policy-model.md +266 -214
  8. package/docs/positioning.md +94 -94
  9. package/docs/reference.md +349 -0
  10. package/docs/routing.md +85 -0
  11. package/docs/user-flow.md +79 -17
  12. package/docs/value-proof.md +194 -0
  13. package/docs/variant-lifecycle.md +86 -0
  14. package/docs/versioning.md +154 -138
  15. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -8
  16. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -8
  17. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -8
  18. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -8
  19. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -8
  20. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -8
  21. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -8
  22. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -8
  23. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -8
  24. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -8
  25. package/examples/skillboard.config.yaml +8 -3
  26. package/examples/skills/grill-me/SKILL.md +9 -9
  27. package/examples/skills/grill-with-docs/SKILL.md +9 -9
  28. package/examples/skills/requirement-intake/SKILL.md +9 -9
  29. package/examples/skills/tdd/SKILL.md +8 -8
  30. package/package.json +6 -3
  31. package/src/advisor/actions.mjs +25 -3
  32. package/src/advisor/guidance.mjs +232 -0
  33. package/src/advisor/schema.mjs +2 -0
  34. package/src/advisor/skills.mjs +2 -0
  35. package/src/advisor/trust-policy.mjs +5 -3
  36. package/src/advisor.mjs +36 -7
  37. package/src/brief-cli.mjs +6 -5
  38. package/src/brief-renderer.mjs +225 -8
  39. package/src/cli.mjs +574 -27
  40. package/src/config-helpers.mjs +34 -18
  41. package/src/conflicts.mjs +70 -0
  42. package/src/control/can-use-guard.mjs +8 -3
  43. package/src/control/skill-crud.mjs +142 -0
  44. package/src/control/skill-variants.mjs +221 -0
  45. package/src/control/source-trust.mjs +1 -0
  46. package/src/control/variant-files.mjs +265 -0
  47. package/src/control/variant-lifecycle-config.mjs +156 -0
  48. package/src/control/variant-reset.mjs +171 -0
  49. package/src/control/variant-status.mjs +75 -0
  50. package/src/control.mjs +13 -1
  51. package/src/domain/rules/skills.mjs +60 -0
  52. package/src/domain/rules/workflows.mjs +13 -0
  53. package/src/hook-plan.mjs +6 -6
  54. package/src/impact.mjs +21 -12
  55. package/src/index.mjs +13 -1
  56. package/src/lifecycle-cli.mjs +19 -8
  57. package/src/lifecycle-content.mjs +34 -24
  58. package/src/route.mjs +537 -0
  59. package/src/source-verification.mjs +7 -3
  60. package/src/workspace.mjs +141 -43
  61. package/tsconfig.lsp.json +1 -1
  62. package/docs/plans/20260625-080025-skillboard-mvp-review.md +0 -189
  63. package/docs/plans/README.md +0 -20
@@ -12,6 +12,7 @@ import { buildSetupGuidanceActions } from "./setup-actions.mjs";
12
12
  import { trustRecommendationAction } from "./trust-policy.mjs";
13
13
 
14
14
  const WRITABLE_MODES = new Set(["manual-only", "router-only", "workflow-auto"]);
15
+ const NON_ACTIVATABLE_STATUSES = new Set(["deprecated", "archived", "removed"]);
15
16
 
16
17
  export function buildInitActions(paths) {
17
18
  return [makeAction({
@@ -82,7 +83,10 @@ function reviewInstallUnitActions({ paths, workflow, reviewQueue }) {
82
83
  }
83
84
 
84
85
  function activateSkillActions({ paths, workflow, skills, workspace }) {
85
- const candidates = [...skills.not_in_workflow, ...skills.blocked.filter((skill) => missingProvenance(workspace, skill.id))];
86
+ const candidates = [
87
+ ...skills.not_in_workflow,
88
+ ...skills.blocked.filter((skill) => missingProvenance(workspace, skill.id) || canActivate(skill))
89
+ ];
86
90
  return candidates.flatMap((skill) => {
87
91
  if (missingProvenance(workspace, skill.id)) {
88
92
  return [blockedActivateAction(paths, workflow, skill)];
@@ -90,7 +94,7 @@ function activateSkillActions({ paths, workflow, skills, workspace }) {
90
94
  if (!canActivate(skill)) {
91
95
  return [];
92
96
  }
93
- const mode = skill.advanced.invocation;
97
+ const mode = activationMode(skill);
94
98
  const risk = riskFromSkill(skill);
95
99
  const dryRun = command([
96
100
  "skillboard", "activate", skill.id, "--workflow", workflow.selected, "--mode", mode,
@@ -169,6 +173,7 @@ function hookInstallActions({ paths, workflow }) {
169
173
  function removeSkillForceActions({ paths, workflow, skills, workspace }) {
170
174
  return skills.blocked
171
175
  .filter((skill) => !missingProvenance(workspace, skill.id))
176
+ .filter((skill) => !canActivate(skill))
172
177
  .map((skill) => {
173
178
  const dryRun = command([
174
179
  "skillboard", "remove", "skill", skill.id, "--force",
@@ -230,7 +235,24 @@ function blockedActivateAction(paths, workflow, skill) {
230
235
  }
231
236
 
232
237
  function canActivate(skill) {
233
- return WRITABLE_MODES.has(skill.advanced.invocation) && skill.advanced.trust.reviewed === true;
238
+ return activationMode(skill) !== null && skill.advanced.trust.reviewed === true;
239
+ }
240
+
241
+ function activationMode(skill) {
242
+ if (NON_ACTIVATABLE_STATUSES.has(skill.advanced.status)) {
243
+ return null;
244
+ }
245
+ if (WRITABLE_MODES.has(skill.advanced.invocation)) {
246
+ return skill.advanced.invocation;
247
+ }
248
+ if (
249
+ skill.advanced.owner_install_unit !== undefined
250
+ && skill.advanced.owner_install_unit !== null
251
+ && (skill.advanced.status === "quarantined" || skill.advanced.invocation === "blocked")
252
+ ) {
253
+ return "manual-only";
254
+ }
255
+ return null;
234
256
  }
235
257
 
236
258
  function missingProvenance(workspace, skillId) {
@@ -0,0 +1,232 @@
1
+ import { command } from "./action-core.mjs";
2
+
3
+ const GUARD_WHEN = "before invoking a skill";
4
+ const GOAL_DOCUMENT = Object.freeze({
5
+ path: "docs/ai-skill-routing-goal.md",
6
+ purpose: "Preserve SkillBoard as a non-blocking AI skill routing control plane: route and work first when safe, explain briefly, ask after use when policy learning helps, and remember usage policy without rewriting skill bodies.",
7
+ when_to_read: Object.freeze([
8
+ "before changing routing",
9
+ "before changing brief output",
10
+ "before changing bridge instructions",
11
+ "before changing policy UX",
12
+ "before changing workflow UX"
13
+ ])
14
+ });
15
+ const GUARD_ALLOWED_USE = Object.freeze({
16
+ confirmation_required: false,
17
+ start: "State at the start which selected skill is being used for this request.",
18
+ finish: "State at completion which selected skill was used.",
19
+ start_message_template: "I will use <skill-id> for this request.",
20
+ finish_message_template: "I used <skill-id> for this request.",
21
+ ask_user_when: "Ask the user only if the guard denies use or a policy-changing action is needed."
22
+ });
23
+
24
+ export function buildAssistantGuidance(brief, options = {}) {
25
+ const status = guidanceStatus(brief);
26
+ const choices = status === "invalid-config" || hasPolicyErrors(brief) ? [] : choicesFromActions(brief.actions ?? []);
27
+ const route = options.route === undefined ? null : routeGuidance(options.route);
28
+ const guidance = {
29
+ status,
30
+ summary: summaryForStatus(status, brief),
31
+ goal_document: goalDocument(),
32
+ recommended_next_step: recommendedNextStep(status, brief, choices, route),
33
+ choices,
34
+ guard: {
35
+ required: true,
36
+ when: GUARD_WHEN,
37
+ command_hint: guardCommandHint(brief),
38
+ allowed_use: GUARD_ALLOWED_USE
39
+ }
40
+ };
41
+ if (route !== null) {
42
+ guidance.route = route;
43
+ }
44
+ return guidance;
45
+ }
46
+
47
+ function goalDocument() {
48
+ return {
49
+ ...GOAL_DOCUMENT,
50
+ when_to_read: [...GOAL_DOCUMENT.when_to_read]
51
+ };
52
+ }
53
+
54
+ function guidanceStatus(brief) {
55
+ if (hasInvalidConfig(brief)) {
56
+ return "invalid-config";
57
+ }
58
+ if (brief.error?.code === "not-initialized") {
59
+ return "not-initialized";
60
+ }
61
+ if (brief.error?.code === "unknown-workflow" || brief.workflow?.unknown === true) {
62
+ return "unknown-workflow";
63
+ }
64
+ if (brief.workflow?.needs_selection === true || brief.workflow?.selected === null) {
65
+ return "workflow-selection-needed";
66
+ }
67
+ if (hasPolicyErrors(brief)) {
68
+ return "blocked";
69
+ }
70
+ if ((brief.review_queue ?? []).length > 0 || brief.health?.review_required === true) {
71
+ return "needs-decision";
72
+ }
73
+ if (brief.health?.policy?.ok === false || brief.ok === false) {
74
+ return "blocked";
75
+ }
76
+ return "ready";
77
+ }
78
+
79
+ function hasInvalidConfig(brief) {
80
+ return brief.error?.code === "invalid-config"
81
+ || (brief.health?.config?.exists === true && brief.health.config.valid === false);
82
+ }
83
+
84
+ function summaryForStatus(status, brief) {
85
+ const readyCount = (brief.skills?.automatic_allowed?.length ?? 0) + (brief.skills?.manual_allowed?.length ?? 0);
86
+ const decisionCount = guidanceDecisionCount(brief);
87
+ const blockedCount = brief.skills?.blocked?.length ?? 0;
88
+ switch (status) {
89
+ case "ready":
90
+ return `SkillBoard is ready; ${readyCount} skills are available in this workflow.`;
91
+ case "needs-decision":
92
+ return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before this workflow is fully ready.`;
93
+ case "blocked":
94
+ return `SkillBoard found blocking policy issues; ${blockedCount} skills are blocked for safety.`;
95
+ case "not-initialized":
96
+ return "SkillBoard is not initialized in this project.";
97
+ case "invalid-config":
98
+ return "SkillBoard cannot read the project configuration.";
99
+ case "workflow-selection-needed":
100
+ return "SkillBoard needs a workflow selection before applying action cards.";
101
+ case "unknown-workflow":
102
+ return `SkillBoard does not know workflow ${brief.workflow?.selected ?? "the requested workflow"}.`;
103
+ default:
104
+ return "SkillBoard could not determine the current guidance state.";
105
+ }
106
+ }
107
+
108
+ function guidanceDecisionCount(brief) {
109
+ const skillDecisionCount = brief.skills?.needs_review?.length ?? 0;
110
+ if (skillDecisionCount > 0) {
111
+ return skillDecisionCount;
112
+ }
113
+ const reviewCount = brief.review_queue?.length ?? 0;
114
+ return reviewCount === 0 ? (brief.actions?.length ?? 0) : reviewCount;
115
+ }
116
+
117
+ function decisionWord(count) {
118
+ return count === 1 ? "decision" : "decisions";
119
+ }
120
+
121
+ function recommendedNextStep(status, brief, choices, route = null) {
122
+ const firstChoice = choices[0];
123
+ switch (status) {
124
+ case "ready":
125
+ if (route !== null) {
126
+ return route.recommended_skill === null
127
+ ? "Ask a clarifying question; no workflow capability matched this request."
128
+ : `Use ${route.recommended_skill} for this request after the guard check passes.`;
129
+ }
130
+ return "Run the guard check before invoking any selected skill.";
131
+ case "needs-decision":
132
+ if (route?.recommended_skill !== null && route?.guard_allowed === true) {
133
+ return `Use ${route.recommended_skill} for this request after the guard check passes; handle pending review decisions after the task unless a policy-changing action is needed now.`;
134
+ }
135
+ return firstChoice === undefined
136
+ ? "Ask the user which pending review decision to make."
137
+ : `Ask the user whether to approve: ${firstChoice.label}.`;
138
+ case "blocked":
139
+ if (hasPolicyErrors(brief)) {
140
+ return "Fix the SkillBoard policy errors before applying actions or invoking skills.";
141
+ }
142
+ return firstChoice === undefined
143
+ ? "Resolve the blocking policy issue before invoking skills."
144
+ : `Review the blocked item before applying: ${firstChoice.label}.`;
145
+ case "not-initialized":
146
+ return firstChoice === undefined
147
+ ? "Initialize SkillBoard before checking skill availability."
148
+ : `Ask the user whether to approve: ${firstChoice.label}.`;
149
+ case "invalid-config":
150
+ return "Fix the SkillBoard configuration before checking skill availability.";
151
+ case "workflow-selection-needed":
152
+ return (brief.workflow?.candidates?.length ?? 0) === 0
153
+ ? "Set up SkillBoard by refreshing inventory, then add a harness and workflow before applying action cards."
154
+ : "Ask the user which workflow to use.";
155
+ case "unknown-workflow":
156
+ return "Ask the user to choose one of the configured workflows.";
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function routeGuidance(route) {
163
+ return {
164
+ intent: route.intent,
165
+ workflow: route.workflow,
166
+ matched_capability: route.matched_capability,
167
+ matched_skill: route.matched_skill ?? null,
168
+ match_source: route.match_source,
169
+ confidence: route.confidence,
170
+ matched_terms: route.matched_terms,
171
+ recommendation_reason: route.recommendation_reason,
172
+ recommended_skill: route.recommended_skill,
173
+ fallback_skills: route.fallback_skills,
174
+ route_candidates: (route.route_candidates ?? []).map((candidate) => ({
175
+ skill: candidate.skill,
176
+ role: candidate.role,
177
+ selected: candidate.selected,
178
+ guard_allowed: candidate.guard_allowed,
179
+ guard_reasons: candidate.guard_reasons,
180
+ guard_roles: candidate.guard_roles,
181
+ capability_roles: candidate.capability_roles
182
+ })),
183
+ usage_disclosure: route.usage_disclosure ?? null,
184
+ post_use_policy_suggestion: route.post_use_policy_suggestion ?? null,
185
+ guard_command: route.guard_command,
186
+ guard_allowed: route.guard?.allowed ?? null,
187
+ guard_reasons: route.guard?.reasons ?? [],
188
+ possible_skills: route.possible_skills.map((skill) => ({
189
+ id: skill.id,
190
+ category: skill.category,
191
+ allowed: skill.allowed
192
+ }))
193
+ };
194
+ }
195
+
196
+ function hasPolicyErrors(brief) {
197
+ return (brief.health?.policy?.errors?.length ?? 0) > 0;
198
+ }
199
+
200
+ function choicesFromActions(actions) {
201
+ return actions.filter(confirmableAction).map((action) => ({
202
+ label: action.label,
203
+ action_id: action.id,
204
+ kind: action.kind,
205
+ applies_to: action.applies_to ?? null,
206
+ risk: action.risk,
207
+ requires_confirmation: action.requires_user_confirmation,
208
+ effect: action.reason,
209
+ blocked_reason: action.blocked_reason ?? action.application?.blocked_reason ?? null
210
+ }));
211
+ }
212
+
213
+ function confirmableAction(action) {
214
+ return action.blocked_reason === null
215
+ && (action.application?.blocked_reason ?? null) === null
216
+ && (action.application?.apply ?? null) !== null;
217
+ }
218
+
219
+ function guardCommandHint(brief) {
220
+ if (hasInvalidConfig(brief)) {
221
+ return null;
222
+ }
223
+ if (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true) {
224
+ return null;
225
+ }
226
+ return command([
227
+ "skillboard", "guard", "use", "<skill-id>",
228
+ "--workflow", brief.workflow.selected,
229
+ "--config", brief.health.config_path,
230
+ "--skills", brief.health.skills_root
231
+ ]).display;
232
+ }
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, resolve } from "node:path";
2
2
  import { uninstallProject } from "../uninstall.mjs";
3
+ import { buildAssistantGuidance } from "./guidance.mjs";
3
4
  import { sortedStrings } from "./sort.mjs";
4
5
 
5
6
  const SCHEMA_VERSION = 1;
@@ -29,6 +30,7 @@ export function buildBrief(data) {
29
30
  if (data.actions !== undefined) {
30
31
  brief.actions = data.actions;
31
32
  }
33
+ brief.assistant_guidance = buildAssistantGuidance(brief, { route: data.route });
32
34
  return brief;
33
35
  }
34
36
 
@@ -52,6 +52,7 @@ function declaredSkillEntry(summary, explanation, use, group) {
52
52
  invocation: summary.invocation,
53
53
  exposure: summary.exposure,
54
54
  category: summary.category,
55
+ variant: summary.variant ?? null,
55
56
  source_class: explanation.source.class,
56
57
  owner_install_unit: explanation.source.ownerInstallUnit,
57
58
  workflow_roles: use?.roles ?? summary.workflowRoles,
@@ -165,6 +166,7 @@ function isHardReason(reason) {
165
166
  return normalized.includes("policy check failed")
166
167
  || normalized.includes("unknown skill")
167
168
  || normalized.includes("unknown workflow")
169
+ || normalized.includes("conflicts with active skill")
168
170
  || normalized.includes("blocks skill")
169
171
  || normalized.includes("source trust policy")
170
172
  || (normalized.includes("install unit") && normalized.includes("disabled"))
@@ -21,9 +21,11 @@ export function recommendTrustLevel(unit) {
21
21
  return "trusted";
22
22
  }
23
23
 
24
- // High-risk sources should not be activated from advisor suggestions without explicit blocking/review.
24
+ // High-risk sources require explicit review, not a default block. Blocking is
25
+ // still available as a recorded trust policy, but the advisor keeps the work
26
+ // moving by asking for a one-time source review first.
25
27
  if (risk === "high") {
26
- return "blocked";
28
+ return "reviewed";
27
29
  }
28
30
 
29
31
  // Unknown risk always requires at least a review before any automatic use.
@@ -54,7 +56,7 @@ export function trustRecommendationAction(recommended) {
54
56
  return {
55
57
  kind: "block-install-unit",
56
58
  label: "Decide whether to block source",
57
- reason: "High-risk or runtime-extending source needs a one-time decision before its skills appear again."
59
+ reason: "Source is blocked by trust policy and needs an explicit decision before its skills appear again."
58
60
  };
59
61
  }
60
62
  return {
package/src/advisor.mjs CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  listWorkflows
4
4
  } from "./control.mjs";
5
5
  import { doctorProject } from "./doctor.mjs";
6
+ import { routeSkill } from "./route.mjs";
6
7
  import { buildActionCards, buildInitActions } from "./advisor/actions.mjs";
7
8
  import {
8
9
  buildBrief,
@@ -52,7 +53,7 @@ export async function buildSkillBrief(options = {}) {
52
53
  return buildExpectedConfigError(configDoctor, paths, cleanup, {
53
54
  code: "invalid-config",
54
55
  message: configDoctor.config.error ?? "skillboard.config.yaml is invalid"
55
- });
56
+ }, options);
56
57
  }
57
58
 
58
59
  let workspace;
@@ -62,7 +63,7 @@ export async function buildSkillBrief(options = {}) {
62
63
  return buildExpectedConfigError(configDoctor, paths, cleanup, {
63
64
  code: "invalid-config",
64
65
  message: error instanceof Error ? error.message : String(error)
65
- });
66
+ }, options);
66
67
  }
67
68
 
68
69
  const doctor = await doctorProject({
@@ -97,19 +98,46 @@ export async function buildSkillBrief(options = {}) {
97
98
 
98
99
  const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit);
99
100
  const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
101
+ const route = routeForBrief({ options, paths, workflow, workspace });
102
+ const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.sources.ok;
100
103
  return buildBrief({
101
- ok: doctor.ok,
102
- health: healthFromDoctor(doctor, paths),
104
+ ok: availabilityOk,
105
+ health: healthForBrief(doctor, paths, availabilityOk),
103
106
  workflow,
104
107
  skills,
105
108
  sources: summarizeSources(sourceAudit),
106
109
  reviewQueue: actionData.reviewQueue,
107
110
  cleanup,
108
- actions: actionData.actions
111
+ actions: actionData.actions,
112
+ route
109
113
  });
110
114
  }
111
115
 
112
- function buildExpectedConfigError(doctor, paths, cleanup, error) {
116
+ function routeForBrief({ options, paths, workflow, workspace }) {
117
+ const intent = options.intent?.trim();
118
+ if (intent === undefined || intent.length === 0 || workflow.selected === null || workflow.unknown || workflow.needs_selection) {
119
+ return undefined;
120
+ }
121
+ return routeSkill(workspace, {
122
+ intent,
123
+ workflow: workflow.selected,
124
+ configPath: paths.configPath,
125
+ skillsRoot: paths.skillsRoot
126
+ });
127
+ }
128
+
129
+ function healthForBrief(doctor, paths, availabilityOk) {
130
+ const health = healthFromDoctor(doctor, paths);
131
+ if (!availabilityOk || health.mode !== "failed") {
132
+ return health;
133
+ }
134
+ return {
135
+ ...health,
136
+ mode: health.review_required ? "safe-mode" : "passed"
137
+ };
138
+ }
139
+
140
+ function buildExpectedConfigError(doctor, paths, cleanup, error, options = {}) {
113
141
  return buildBrief({
114
142
  ok: false,
115
143
  error,
@@ -118,7 +146,8 @@ function buildExpectedConfigError(doctor, paths, cleanup, error) {
118
146
  skills: emptySkillGroups(),
119
147
  sources: sourcesFromDoctor(doctor),
120
148
  reviewQueue: [],
121
- cleanup
149
+ cleanup,
150
+ actions: requestedActions(options) ? [] : undefined
122
151
  });
123
152
  }
124
153
 
package/src/brief-cli.mjs CHANGED
@@ -6,11 +6,12 @@ export async function runBriefCommand(options, stdout, paths) {
6
6
  const json = options.get("json") === "true";
7
7
  const result = await buildSkillBrief({
8
8
  root: briefRoot(options),
9
- configPath: paths.configPath,
10
- skillsRoot: paths.skillsRoot,
11
- workflow: options.get("workflow"),
12
- includeActions: options.get("include-actions") === "true" || !json
13
- });
9
+ configPath: paths.configPath,
10
+ skillsRoot: paths.skillsRoot,
11
+ workflow: options.get("workflow"),
12
+ intent: options.get("intent"),
13
+ includeActions: options.get("include-actions") === "true" || !json
14
+ });
14
15
  writeBriefOutput(stdout, result, options);
15
16
  return briefExitCode(result);
16
17
  }