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
@@ -1,6 +1,5 @@
1
+ // SIZE_OK: src/brief-renderer.mjs is pre-existing renderer debt; this change only adds narrow AI/automation copy until a broader renderer split.
1
2
  const SKILL_SECTIONS = [
2
- ["What your AI can use now", "automatic_allowed"],
3
- ["Manual only", "manual_allowed"],
4
3
  ["Needs your decision", "needs_review"],
5
4
  ["Blocked for safety", "blocked"]
6
5
  ];
@@ -20,6 +19,7 @@ const CLEANUP_ACTION_KINDS = new Set([
20
19
  ]);
21
20
  const COMPACT_SKILL_LIMIT = 5;
22
21
  const TOP_CATEGORY_LIMIT = 5;
22
+ const POLICY_DIAGNOSTIC_LIMIT = 3;
23
23
  const MAX_ACTIONS_PER_TEXT_SECTION = 5;
24
24
  const ACTION_KIND_RANK = new Map([
25
25
  ["setup-guidance", 0],
@@ -48,9 +48,16 @@ export function renderSkillBrief(brief, options = {}) {
48
48
  if (brief.error !== undefined) {
49
49
  lines.push(`Error: ${safeText(brief.error.message)}`, "");
50
50
  }
51
+ emitIntentRoute(lines, brief);
52
+ emitPolicyHealth(lines, brief);
51
53
  emitCategorySummary(lines, brief);
52
54
  emitNextAction(lines, brief, { verbose });
55
+ emitAvailableNowSection(lines, brief, { verbose });
53
56
  for (const [title, key] of SKILL_SECTIONS) {
57
+ if (key === "needs_review") {
58
+ emitDecisionSection(lines, brief, { verbose });
59
+ continue;
60
+ }
54
61
  emitSkillSection(lines, title, brief.skills[key], {
55
62
  brief,
56
63
  groupLabel: compactGroupLabel(key),
@@ -69,6 +76,109 @@ export function renderSkillBrief(brief, options = {}) {
69
76
  return `${lines.join("\n")}\n`;
70
77
  }
71
78
 
79
+ function emitIntentRoute(lines, brief) {
80
+ const route = brief.assistant_guidance?.route;
81
+ if (route === undefined) {
82
+ return;
83
+ }
84
+ lines.push("## Suggested skill for this request", "");
85
+ lines.push(`- Intent: ${safeText(route.intent)}`);
86
+ lines.push(`- Match source: ${route.match_source}`);
87
+ lines.push(`- Matched capability: ${route.matched_capability ?? "none"}`);
88
+ lines.push(`- Matched skill: ${route.matched_skill === null ? "none" : code(route.matched_skill)}`);
89
+ lines.push(`- Confidence: ${route.confidence}`);
90
+ lines.push(`- Why: ${safeText(route.recommendation_reason, 320)}`);
91
+ lines.push(`- Matched terms: ${formatCodeList(route.matched_terms)}`);
92
+ if (route.recommended_skill === null) {
93
+ lines.push("- Recommended skill: none");
94
+ lines.push("- Next step: ask a clarifying question before choosing a skill.");
95
+ } else {
96
+ lines.push(`- Recommended skill: ${code(route.recommended_skill)}`);
97
+ lines.push(`- Fallback skills: ${formatCodeList(route.fallback_skills)}`);
98
+ if ((route.route_candidates ?? []).length > 0) {
99
+ lines.push("- Route candidates:");
100
+ for (const candidate of route.route_candidates) {
101
+ lines.push(` - ${code(candidate.skill)} (${routeCandidateStatus(candidate)})`);
102
+ if (!candidate.guard_allowed && candidate.guard_reasons.length > 0) {
103
+ lines.push(` - ${safeText(candidate.guard_reasons[0])}`);
104
+ }
105
+ }
106
+ }
107
+ lines.push(`- Guard: ${code(route.guard_command, Number.POSITIVE_INFINITY)}`);
108
+ if (route.usage_disclosure !== null && route.usage_disclosure !== undefined) {
109
+ lines.push(`- Disclosure: ${routeDisclosureText(code(route.recommended_skill))}`);
110
+ lines.push(`- Say before use: "${safeText(route.usage_disclosure.start_message)}"`);
111
+ lines.push(`- Say after completion: "${safeText(route.usage_disclosure.finish_message)}"`);
112
+ }
113
+ emitPostUsePolicySuggestion(lines, route.post_use_policy_suggestion);
114
+ }
115
+ lines.push("");
116
+ }
117
+
118
+ function routeCandidateStatus(candidate) {
119
+ return [
120
+ candidate.role,
121
+ candidate.selected ? "selected" : null,
122
+ candidate.guard_allowed ? "allowed" : "denied"
123
+ ].filter((value) => value !== null).join(", ");
124
+ }
125
+
126
+ function routeDisclosureText(skillLabel) {
127
+ return `run the guard automatically, state at the start that ${skillLabel} is being used, and state at completion that it was used. No extra user approval is needed when the guard allows it.`;
128
+ }
129
+
130
+ function emitPostUsePolicySuggestion(lines, suggestion) {
131
+ if (suggestion === null || suggestion === undefined) {
132
+ return;
133
+ }
134
+ lines.push(`- After completion: ${safeText(afterUsePromptText(suggestion.question))}`);
135
+ lines.push(`- Policy command after confirmation: ${code(suggestion.suggested_policy.command_hint, Number.POSITIVE_INFINITY)}`);
136
+ }
137
+
138
+ function afterUsePromptText(question) {
139
+ return question.replace(/^Should I /u, "ask whether to ").replace(/\?$/u, ".");
140
+ }
141
+
142
+ function emitPolicyHealth(lines, brief) {
143
+ const policy = brief.health?.policy;
144
+ if (policy === undefined) {
145
+ return;
146
+ }
147
+ const errors = policy.errors ?? [];
148
+ const warnings = policy.warnings ?? [];
149
+ if (errors.length === 0 && warnings.length === 0) {
150
+ return;
151
+ }
152
+ lines.push("## Policy health", "");
153
+ emitPolicyDiagnostics(lines, "Policy errors", errors);
154
+ emitPolicyDiagnostics(lines, "Policy warnings", warnings);
155
+ lines.push(`- check with: ${code(policyCheckCommand(brief), Number.POSITIVE_INFINITY)}`);
156
+ lines.push("");
157
+ }
158
+
159
+ function emitPolicyDiagnostics(lines, label, diagnostics) {
160
+ if (diagnostics.length === 0) {
161
+ return;
162
+ }
163
+ lines.push(`- ${label}: ${diagnostics.length}`);
164
+ for (const diagnostic of diagnostics.slice(0, POLICY_DIAGNOSTIC_LIMIT)) {
165
+ lines.push(` - ${safeText(diagnostic)}`);
166
+ }
167
+ const hidden = diagnostics.length - POLICY_DIAGNOSTIC_LIMIT;
168
+ if (hidden > 0) {
169
+ lines.push(` - ${hidden} more hidden. Run ${code("skillboard check")}.`);
170
+ }
171
+ }
172
+
173
+ function policyCheckCommand(brief) {
174
+ const config = brief.health?.config_path;
175
+ const skills = brief.health?.skills_root;
176
+ if (config === undefined || skills === undefined) {
177
+ return "skillboard check";
178
+ }
179
+ return `skillboard check --config ${config} --skills ${skills}`;
180
+ }
181
+
72
182
  function briefCounts(brief) {
73
183
  const automatic = brief.skills.automatic_allowed.length;
74
184
  const manual = brief.skills.manual_allowed.length;
@@ -76,32 +186,133 @@ function briefCounts(brief) {
76
186
  automatic,
77
187
  manual,
78
188
  usable: automatic + manual,
79
- needsDecision: brief.skills.needs_review.length,
189
+ needsDecision: decisionCount(brief),
80
190
  blocked: brief.skills.blocked.length
81
191
  };
82
192
  }
83
193
 
194
+ function decisionCount(brief) {
195
+ const skillDecisionCount = brief.skills.needs_review.length;
196
+ if (skillDecisionCount > 0) {
197
+ return skillDecisionCount;
198
+ }
199
+ return brief.review_queue?.length ?? 0;
200
+ }
201
+
202
+ function emitAvailableNowSection(lines, brief, options) {
203
+ lines.push("## What your AI can use now", "");
204
+ const entries = [
205
+ ...brief.skills.automatic_allowed.map((entry) => ({ entry, mode: "automatic" })),
206
+ ...brief.skills.manual_allowed.map((entry) => ({ entry, mode: "manual-only" }))
207
+ ];
208
+ if (entries.length === 0) {
209
+ lines.push("- none", "");
210
+ return;
211
+ }
212
+ lines.push("Automatic skills can be selected by the AI. On-request skills can be used when the user asks the AI; the AI runs the guard first.");
213
+ lines.push("When the guard allows use, disclose the selected skill at the start and completion instead of asking again.", "");
214
+ const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
215
+ for (const item of visibleEntries) {
216
+ lines.push(formatSkillEntry(item.entry, [availableModeLabel(item.mode)]));
217
+ }
218
+ const hiddenEntries = entries.slice(visibleEntries.length);
219
+ if (hiddenEntries.length > 0) {
220
+ lines.push(`- ${hiddenEntries.length} more ${availableHiddenLabel(hiddenEntries)} hidden. Run ${code(verboseCommand(brief))} or ${code(listCommand(brief))}.`);
221
+ }
222
+ lines.push("");
223
+ }
224
+
225
+ function availableHiddenLabel(entries) {
226
+ const modes = new Set(entries.map((entry) => entry.mode));
227
+ if (modes.size !== 1) {
228
+ return "available skills";
229
+ }
230
+ return modes.has("manual-only") ? "on-request skills" : "automatic skills";
231
+ }
232
+
233
+ function availableModeLabel(mode) {
234
+ return mode === "manual-only" ? "on request" : mode;
235
+ }
236
+
84
237
  function emitSkillSection(lines, title, entries, options) {
85
238
  lines.push(`## ${title}`, "");
86
239
  if (entries.length === 0) {
87
240
  lines.push("- none", "");
88
241
  return;
89
242
  }
243
+ emitSkillEntries(lines, entries, options);
244
+ lines.push("");
245
+ }
246
+
247
+ function emitSkillEntries(lines, entries, options) {
90
248
  const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
91
249
  for (const entry of visibleEntries) {
92
- const path = entry.path === undefined ? "" : ` (${safeText(entry.path)})`;
93
- const reason = entry.reason === null || entry.reason === undefined
94
- ? ""
95
- : ` - ${safeText(entry.reason)}`;
96
- lines.push(`- ${code(entry.id)}${path}${reason}`);
250
+ lines.push(formatSkillEntry(entry));
97
251
  }
98
252
  const hidden = entries.length - visibleEntries.length;
99
253
  if (hidden > 0) {
100
254
  lines.push(`- ${hidden} more ${options.groupLabel} hidden. Run ${code(verboseCommand(options.brief))} or ${code(listCommand(options.brief))}.`);
101
255
  }
256
+ }
257
+
258
+ function formatSkillEntry(entry, labels = []) {
259
+ const path = entry.path === undefined ? "" : ` (${safeText(entry.path)})`;
260
+ const reason = entry.reason === null || entry.reason === undefined ? null : safeText(entry.reason);
261
+ const details = [...labels, reason].filter((value) => value !== null && value !== "");
262
+ const suffix = details.length === 0 ? "" : ` - ${details.join("; ")}`;
263
+ return `- ${code(entry.id)}${path}${suffix}`;
264
+ }
265
+
266
+ function emitDecisionSection(lines, brief, options) {
267
+ lines.push("## Needs your decision", "");
268
+ const skillEntries = brief.skills.needs_review;
269
+ const reviewEntries = reviewEntriesForDecisionSection(brief, skillEntries);
270
+ if (skillEntries.length === 0 && reviewEntries.length === 0) {
271
+ lines.push("- none", "");
272
+ return;
273
+ }
274
+ if (skillEntries.length > 0) {
275
+ emitSkillEntries(lines, skillEntries, {
276
+ ...options,
277
+ brief,
278
+ groupLabel: "decision items"
279
+ });
280
+ }
281
+ if (reviewEntries.length > 0) {
282
+ emitReviewQueueEntries(lines, reviewEntries, {
283
+ ...options,
284
+ brief
285
+ });
286
+ }
102
287
  lines.push("");
103
288
  }
104
289
 
290
+ function reviewEntriesForDecisionSection(brief, skillEntries) {
291
+ if (skillEntries.length === 0) {
292
+ return brief.review_queue ?? [];
293
+ }
294
+ return [];
295
+ }
296
+
297
+ function emitReviewQueueEntries(lines, entries, options) {
298
+ const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
299
+ for (const entry of visibleEntries) {
300
+ const label = entry.label ?? entry.title ?? entry.id;
301
+ const action = firstActionId(entry);
302
+ const actionText = action === null ? "" : ` - action: ${code(action)}`;
303
+ lines.push(`- ${safeText(label)} - ${safeText(entry.reason)}${actionText}`);
304
+ }
305
+ const hidden = entries.length - visibleEntries.length;
306
+ if (hidden > 0) {
307
+ lines.push(`- ${hidden} more review decisions hidden. Run ${code(verboseCommand(options.brief))}.`);
308
+ }
309
+ }
310
+
311
+ function firstActionId(entry) {
312
+ const [action] = entry.action_ids ?? [];
313
+ return typeof action === "string" ? action : null;
314
+ }
315
+
105
316
  function emitCategorySummary(lines, brief) {
106
317
  const categories = categoryCounts(brief);
107
318
  lines.push("## Top categories", "");
@@ -173,6 +384,7 @@ function workflowOption(brief) {
173
384
  function emitActions(lines, brief, options) {
174
385
  const actions = actionsForTextBrief(brief);
175
386
  lines.push("## Suggested next actions", "");
387
+ lines.push("AI/automation operations should use current action ids from this brief, then ask for user confirmation before applying one action.", "");
176
388
  if (actions.length === 0) {
177
389
  lines.push("- none", "");
178
390
  return;
@@ -209,6 +421,7 @@ function emitActions(lines, brief, options) {
209
421
 
210
422
  function emitNextAction(lines, brief, options) {
211
423
  lines.push("## Next safe action", "");
424
+ lines.push("AI/automation should present this as the next confirmable operation, not as an automatic mutation.", "");
212
425
  const action = nextSafeAction(brief);
213
426
  if (action === null) {
214
427
  lines.push("- none", "");
@@ -360,3 +573,7 @@ function safeText(value, maxLength = 180) {
360
573
  function code(value, maxLength = 180) {
361
574
  return `\`${safeText(value, maxLength)}\``;
362
575
  }
576
+
577
+ function formatCodeList(values) {
578
+ return values.length === 0 ? "none" : values.map((value) => code(value)).join(", ");
579
+ }