agent-skillboard 0.1.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 (94) hide show
  1. package/CONTRIBUTING.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +645 -0
  4. package/bin/skillboard.mjs +4 -0
  5. package/docs/adapters.md +127 -0
  6. package/docs/capabilities.md +107 -0
  7. package/docs/install.md +346 -0
  8. package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
  9. package/docs/plans/README.md +20 -0
  10. package/docs/policy-model.md +251 -0
  11. package/docs/positioning.md +94 -0
  12. package/docs/profiles.md +166 -0
  13. package/docs/rollout-runbook.md +60 -0
  14. package/docs/user-flow.md +231 -0
  15. package/docs/versioning.md +201 -0
  16. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
  17. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
  18. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
  19. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
  20. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
  21. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
  22. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
  23. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
  24. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
  25. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
  26. package/examples/multi-source.config.yaml +271 -0
  27. package/examples/skillboard.config.yaml +194 -0
  28. package/examples/skills/grill-me/SKILL.md +9 -0
  29. package/examples/skills/grill-with-docs/SKILL.md +9 -0
  30. package/examples/skills/requirement-intake/SKILL.md +9 -0
  31. package/examples/skills/tdd/SKILL.md +8 -0
  32. package/package.json +58 -0
  33. package/profiles/anthropics-skills.yaml +17 -0
  34. package/profiles/mattpocock-skills.yaml +26 -0
  35. package/profiles/oh-my-openagent.yaml +32 -0
  36. package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
  37. package/profiles/wshobson-agents.yaml +19 -0
  38. package/src/advisor/action-core.mjs +74 -0
  39. package/src/advisor/actions.mjs +256 -0
  40. package/src/advisor/application-commands.mjs +59 -0
  41. package/src/advisor/apply-action.mjs +183 -0
  42. package/src/advisor/schema.mjs +112 -0
  43. package/src/advisor/setup-actions.mjs +42 -0
  44. package/src/advisor/skills.mjs +191 -0
  45. package/src/advisor/sort.mjs +15 -0
  46. package/src/advisor/sources.mjs +160 -0
  47. package/src/advisor/trust-policy.mjs +65 -0
  48. package/src/advisor/workflow.mjs +41 -0
  49. package/src/advisor.mjs +134 -0
  50. package/src/agent-inventory-platforms.mjs +100 -0
  51. package/src/agent-inventory.mjs +804 -0
  52. package/src/brief-cli.mjs +40 -0
  53. package/src/brief-renderer.mjs +362 -0
  54. package/src/change-plan.mjs +169 -0
  55. package/src/cli.mjs +1171 -0
  56. package/src/config-helpers.mjs +72 -0
  57. package/src/control/can-use-guard.mjs +91 -0
  58. package/src/control/config-write.mjs +163 -0
  59. package/src/control/skill-crud.mjs +394 -0
  60. package/src/control/source-trust.mjs +161 -0
  61. package/src/control/workflow-crud.mjs +104 -0
  62. package/src/control.mjs +197 -0
  63. package/src/doctor.mjs +299 -0
  64. package/src/domain/constants.mjs +47 -0
  65. package/src/domain/indexes.mjs +29 -0
  66. package/src/domain/rules/capabilities.mjs +33 -0
  67. package/src/domain/rules/harnesses.mjs +16 -0
  68. package/src/domain/rules/install-units.mjs +95 -0
  69. package/src/domain/rules/skills.mjs +105 -0
  70. package/src/domain/rules/workflows.mjs +105 -0
  71. package/src/domain/skill-state-matrix.mjs +79 -0
  72. package/src/domain/source-classes.mjs +99 -0
  73. package/src/hook-plan.mjs +152 -0
  74. package/src/impact.mjs +41 -0
  75. package/src/index.mjs +82 -0
  76. package/src/init.mjs +136 -0
  77. package/src/install-output-detector.mjs +337 -0
  78. package/src/install-units.mjs +62 -0
  79. package/src/inventory-refresh.mjs +45 -0
  80. package/src/lifecycle-cli.mjs +166 -0
  81. package/src/lifecycle-content.mjs +87 -0
  82. package/src/policy.mjs +42 -0
  83. package/src/reconcile.mjs +111 -0
  84. package/src/report.mjs +151 -0
  85. package/src/review.mjs +88 -0
  86. package/src/rollout.mjs +453 -0
  87. package/src/skill-paths.mjs +17 -0
  88. package/src/source-cache.mjs +178 -0
  89. package/src/source-profile-loader.mjs +160 -0
  90. package/src/source-profiles.mjs +299 -0
  91. package/src/source-verification.mjs +252 -0
  92. package/src/uninstall.mjs +258 -0
  93. package/src/workspace.mjs +219 -0
  94. package/tsconfig.lsp.json +15 -0
@@ -0,0 +1,72 @@
1
+ export function requireRecord(value, label) {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3
+ throw new Error(`${label} must be a mapping`);
4
+ }
5
+ return value;
6
+ }
7
+
8
+ export function readString(record, key, fallback) {
9
+ const value = record[key];
10
+ if (value === undefined) {
11
+ return fallback;
12
+ }
13
+ if (typeof value !== "string") {
14
+ throw new Error(`${key} must be a string`);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ export function readOptionalString(record, key) {
20
+ const value = record[key];
21
+ if (value === undefined) {
22
+ return undefined;
23
+ }
24
+ if (typeof value !== "string") {
25
+ throw new Error(`${key} must be a string`);
26
+ }
27
+ return value;
28
+ }
29
+
30
+ export function readBoolean(record, key, fallback) {
31
+ const value = record[key];
32
+ if (value === undefined) {
33
+ return fallback;
34
+ }
35
+ if (typeof value !== "boolean") {
36
+ throw new Error(`${key} must be a boolean`);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ export function readNumber(record, key, fallback) {
42
+ const value = record[key];
43
+ if (value === undefined) {
44
+ return fallback;
45
+ }
46
+ if (typeof value !== "number" || !Number.isFinite(value)) {
47
+ throw new Error(`${key} must be a number`);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ export function readOptionalNumber(record, key) {
53
+ const value = record[key];
54
+ if (value === undefined) {
55
+ return undefined;
56
+ }
57
+ if (typeof value !== "number" || !Number.isFinite(value)) {
58
+ throw new Error(`${key} must be a number`);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ export function readStringList(record, key) {
64
+ const value = record[key];
65
+ if (value === undefined) {
66
+ return [];
67
+ }
68
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
69
+ throw new Error(`${key} must be a list of strings`);
70
+ }
71
+ return value;
72
+ }
@@ -0,0 +1,91 @@
1
+ import { checkPolicy } from "../policy.mjs";
2
+ import {
3
+ NON_CALLABLE_WORKFLOW_INVOCATIONS,
4
+ NON_CALLABLE_WORKFLOW_STATUSES
5
+ } from "../domain/constants.mjs";
6
+ import { isModelSelectableInvocation, isUserControlledSource } from "../domain/source-classes.mjs";
7
+ import { classifySkillTrust, workflowSkillRole } from "./source-trust.mjs";
8
+
9
+ export function canUseSkill(workspace, skillId, workflowName) {
10
+ const policy = checkPolicy(workspace);
11
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
12
+ const workflow = workspace.workflows.find((candidate) => candidate.name === workflowName);
13
+ const reasons = [];
14
+
15
+ if (!policy.ok) {
16
+ reasons.push("Policy check failed.");
17
+ }
18
+ if (skill === undefined) {
19
+ reasons.push(`Unknown skill: ${skillId}`);
20
+ }
21
+ if (workflow === undefined) {
22
+ reasons.push(`Unknown workflow: ${workflowName}`);
23
+ }
24
+ if (skill === undefined || workflow === undefined) {
25
+ return canUseResult(false, skillId, workflowName, skill, workflow, reasons);
26
+ }
27
+
28
+ const role = workflowSkillRole(workflow, skillId);
29
+ if (workflow.blockedSkills.includes(skillId)) {
30
+ reasons.push(`Workflow ${workflowName} blocks skill ${skillId}.`);
31
+ }
32
+ reasons.push(...trustUseReasons(workspace, skill));
33
+ if (role.roles.length === 0 && skill.invocation !== "global-auto") {
34
+ reasons.push(`Skill ${skillId} is not active, preferred, or fallback in workflow ${workflowName}.`);
35
+ }
36
+ if (NON_CALLABLE_WORKFLOW_STATUSES.has(skill.status)) {
37
+ reasons.push(`Skill ${skillId} has non-callable status: ${skill.status}.`);
38
+ }
39
+ if (NON_CALLABLE_WORKFLOW_INVOCATIONS.has(skill.invocation)) {
40
+ reasons.push(`Skill ${skillId} has non-callable invocation: ${skill.invocation}.`);
41
+ }
42
+ if (skill.invocation === "global-auto" && skill.exposure !== "global-meta") {
43
+ reasons.push(`Skill ${skillId} is global-auto but not global-meta.`);
44
+ }
45
+
46
+ return canUseResult(reasons.length === 0, skillId, workflowName, skill, workflow, reasons, role, classifySkillTrust(workspace, skill));
47
+ }
48
+
49
+ function trustUseReasons(workspace, skill) {
50
+ const unit = skill.ownerInstallUnit === undefined
51
+ ? undefined
52
+ : workspace.installUnits.find((candidate) => candidate.id === skill.ownerInstallUnit);
53
+ if (unit === undefined) {
54
+ return [];
55
+ }
56
+ const trust = classifySkillTrust(workspace, skill);
57
+ const reasons = [];
58
+ if (!unit.enabled) {
59
+ reasons.push(`Install unit ${unit.id} is disabled.`);
60
+ }
61
+ if (trust.level === "blocked") {
62
+ reasons.push(`Install unit ${unit.id} is blocked by source trust policy.`);
63
+ }
64
+ if (unit.enabled && !isUserControlledSource(unit) && trust.level === "unreviewed") {
65
+ reasons.push(`Skill ${skill.id} belongs to unreviewed non-user source ${unit.id}.`);
66
+ }
67
+ if (!isUserControlledSource(unit) && isModelSelectableInvocation(skill.invocation) && trust.level === "unreviewed") {
68
+ reasons.push(`Skill ${skill.id} is model-selectable but source ${unit.id} is unreviewed.`);
69
+ }
70
+ if (skill.invocation === "global-auto" && !trust.reviewed) {
71
+ reasons.push(`Skill ${skill.id} is global-auto but source ${unit.id} is not reviewed or trusted.`);
72
+ }
73
+ return reasons;
74
+ }
75
+
76
+ export function canUseResult(allowed, skillId, workflowName, skill, workflow, reasons, role = { roles: [], capabilityRoles: [] }, trust = null) {
77
+ const automaticAllowed = allowed && ["workflow-auto", "global-auto"].includes(skill?.invocation ?? "");
78
+ return {
79
+ allowed,
80
+ automaticAllowed,
81
+ skill: skillId,
82
+ workflow: workflowName,
83
+ invocation: skill?.invocation ?? null,
84
+ status: skill?.status ?? null,
85
+ workflowKnown: workflow !== undefined,
86
+ trust,
87
+ roles: role.roles,
88
+ capabilityRoles: role.capabilityRoles,
89
+ reasons
90
+ };
91
+ }
@@ -0,0 +1,163 @@
1
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { basename, dirname, join } from "node:path";
4
+ import YAML from "yaml";
5
+ import { loadWorkspace } from "../workspace.mjs";
6
+ import { checkPolicy } from "../policy.mjs";
7
+ import { textChangePlan } from "../change-plan.mjs";
8
+ import { canUseSkill } from "./can-use-guard.mjs";
9
+
10
+ export async function loadConfig(path) {
11
+ const text = await readFile(path, "utf8");
12
+ const document = YAML.parseDocument(text);
13
+ if (document.errors.length > 0) {
14
+ throw new Error(`Invalid YAML config: ${document.errors.map((error) => error.message).join("; ")}`);
15
+ }
16
+ requireYamlMap(document.contents, "config root");
17
+ return {
18
+ document,
19
+ originalText: text
20
+ };
21
+ }
22
+
23
+ export async function writeCheckedConfig(document, originalText, options, message) {
24
+ const nextText = preserveLineEndings(String(document), originalText);
25
+ const plan = textChangePlan(originalText, nextText);
26
+ const tempPath = tempConfigPath(options.configPath);
27
+ await writeFile(tempPath, nextText, { encoding: "utf8", flag: "wx" });
28
+ try {
29
+ const workspace = await loadWorkspace({ configPath: tempPath, skillsRoot: options.skillsRoot });
30
+ const policy = checkPolicy(workspace);
31
+ if (!policy.ok) {
32
+ throw new Error(`Policy update would create invalid config:\n${policy.errors.join("\n")}`);
33
+ }
34
+ const validateUses = options.validateUses ?? (options.validateUse === undefined ? [] : [options.validateUse]);
35
+ for (const useRequest of validateUses) {
36
+ const use = canUseSkill(workspace, useRequest.skillId, useRequest.workflow);
37
+ if (!use.allowed) {
38
+ throw new Error(`Control update would not be usable:\n${use.reasons.join("\n")}`);
39
+ }
40
+ }
41
+ if (options.dryRun === true) {
42
+ return { message, policy, dryRun: true, changed: plan.changed, plan };
43
+ }
44
+ if (plan.changed) {
45
+ await rename(tempPath, options.configPath);
46
+ }
47
+ return { message, policy, dryRun: false, changed: plan.changed, plan };
48
+ } finally {
49
+ await rm(tempPath, { force: true });
50
+ }
51
+ }
52
+
53
+ function tempConfigPath(configPath) {
54
+ return join(dirname(configPath), `.${basename(configPath)}.${randomUUID()}.tmp`);
55
+ }
56
+
57
+ function preserveLineEndings(text, reference) {
58
+ return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
59
+ }
60
+
61
+ export function requireMapAt(document, path, label) {
62
+ return requireYamlMap(document.getIn(path, true), label);
63
+ }
64
+
65
+ export function ensureMapAt(document, path, label) {
66
+ const existing = document.getIn(path, true);
67
+ if (existing !== undefined) {
68
+ const map = requireYamlMap(existing, label);
69
+ map.flow = false;
70
+ return map;
71
+ }
72
+ const next = document.createNode({});
73
+ next.flow = false;
74
+ document.setIn(path, next);
75
+ return next;
76
+ }
77
+
78
+ export function requireYamlMap(value, label) {
79
+ if (!YAML.isMap(value)) {
80
+ throw new Error(`${label} must be a mapping`);
81
+ }
82
+ return value;
83
+ }
84
+
85
+ export function ensureSeq(map, key, document) {
86
+ const existing = map.get(key, true);
87
+ if (existing === undefined) {
88
+ const next = document.createNode([]);
89
+ next.flow = false;
90
+ map.set(key, next);
91
+ return next;
92
+ }
93
+ if (!YAML.isSeq(existing)) {
94
+ throw new Error(`${key} must be a list`);
95
+ }
96
+ existing.flow = false;
97
+ return existing;
98
+ }
99
+
100
+ export function optionalSeq(map, key) {
101
+ const existing = map.get(key, true);
102
+ if (existing === undefined) {
103
+ return undefined;
104
+ }
105
+ if (!YAML.isSeq(existing)) {
106
+ throw new Error(`${key} must be a list`);
107
+ }
108
+ return existing;
109
+ }
110
+
111
+ export function optionalMap(map, key) {
112
+ const existing = map.get(key, true);
113
+ if (existing === undefined) {
114
+ return undefined;
115
+ }
116
+ return requireYamlMap(existing, key);
117
+ }
118
+
119
+ export function mapValues(map, label) {
120
+ return map.items.map((pair) => requireYamlMap(pair.value, label));
121
+ }
122
+
123
+ export function readMapString(map, key, fallback) {
124
+ const value = map.get(key);
125
+ if (value === undefined) {
126
+ return fallback;
127
+ }
128
+ if (typeof value !== "string") {
129
+ throw new Error(`${key} must be a string`);
130
+ }
131
+ return value;
132
+ }
133
+
134
+ export function addUnique(values, value) {
135
+ if (!sequenceIncludes(values, value)) {
136
+ values.add(value);
137
+ }
138
+ }
139
+
140
+ export function removeValue(values, value) {
141
+ for (let index = values.items.length - 1; index >= 0; index -= 1) {
142
+ if (nodeScalarValue(values.items[index]) === value) {
143
+ values.delete(index);
144
+ }
145
+ }
146
+ }
147
+
148
+ export function optionalRootMap(document, key) {
149
+ const existing = document.get(key, true);
150
+ return existing === undefined ? undefined : requireYamlMap(existing, key);
151
+ }
152
+
153
+ export function sequenceIncludes(sequence, value) {
154
+ return sequence.items.some((item) => nodeScalarValue(item) === value);
155
+ }
156
+
157
+ export function uniqueValues(values) {
158
+ return [...new Set(values)];
159
+ }
160
+
161
+ export function nodeScalarValue(node) {
162
+ return node !== null && typeof node === "object" && "value" in node ? node.value : node;
163
+ }