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,197 @@
1
+ import { chmod, lstat, mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import {
4
+ installUnitSourceClass,
5
+ installUnitPriority
6
+ } from "./domain/source-classes.mjs";
7
+ import { canUseSkill } from "./control/can-use-guard.mjs";
8
+ import {
9
+ auditSources,
10
+ classifySkillSource,
11
+ classifySkillTrust,
12
+ skillSummary
13
+ } from "./control/source-trust.mjs";
14
+ import {
15
+ activateSkill,
16
+ addSkill,
17
+ blockSkill,
18
+ preferSkill,
19
+ quarantineSkill,
20
+ removeSkill
21
+ } from "./control/skill-crud.mjs";
22
+ import { addHarness, addWorkflow } from "./control/workflow-crud.mjs";
23
+ import {
24
+ GUARD_HOOK_MODE,
25
+ assertGuardHookPlanIsInstallable,
26
+ buildGuardHookInstallPlan,
27
+ planGuardHookInstall
28
+ } from "./hook-plan.mjs";
29
+
30
+ export { planGuardHookInstall } from "./hook-plan.mjs";
31
+
32
+ export function listSkills(workspace, options = {}) {
33
+ const workflow = options.workflow === undefined ? undefined : workflowByName(workspace, options.workflow);
34
+ const skillIds = workflow === undefined
35
+ ? new Set(workspace.skills.map((skill) => skill.id))
36
+ : new Set([
37
+ ...workflow.activeSkills,
38
+ ...workflow.blockedSkills,
39
+ ...workflow.requiredCapabilities.flatMap((capability) => {
40
+ return [capability.preferred, ...capability.fallback].filter((skillId) => skillId.length > 0);
41
+ })
42
+ ]);
43
+
44
+ return workspace.skills
45
+ .filter((skill) => skillIds.has(skill.id))
46
+ .map((skill) => skillSummary(workspace, skill, workflow))
47
+ .sort((left, right) => {
48
+ return right.sourcePriority - left.sourcePriority || left.id.localeCompare(right.id);
49
+ });
50
+ }
51
+
52
+ export function listWorkflows(workspace) {
53
+ return workspace.workflows.map((workflow) => ({
54
+ name: workflow.name,
55
+ harness: workflow.harness,
56
+ activeSkills: workflow.activeSkills,
57
+ blockedSkills: workflow.blockedSkills,
58
+ requiredCapabilities: workflow.requiredCapabilities.map((capability) => capability.name)
59
+ })).sort((left, right) => left.name.localeCompare(right.name));
60
+ }
61
+
62
+ export function listHarnesses(workspace) {
63
+ return workspace.harnesses.map((harness) => ({
64
+ name: harness.name,
65
+ status: harness.status,
66
+ workflows: harness.workflows,
67
+ commands: harness.commands
68
+ })).sort((left, right) => left.name.localeCompare(right.name));
69
+ }
70
+
71
+ export function listInstallUnits(workspace) {
72
+ return workspace.installUnits.map((unit) => ({
73
+ id: unit.id,
74
+ kind: unit.kind,
75
+ sourceClass: installUnitSourceClass(unit),
76
+ priority: installUnitPriority(unit),
77
+ trustLevel: unit.trustLevel,
78
+ sourceDigest: unit.sourceDigest ?? null,
79
+ signature: unit.signature ?? null,
80
+ publicKey: unit.publicKey ?? null,
81
+ verifiedAt: unit.verifiedAt ?? null,
82
+ source: unit.source,
83
+ scope: unit.scope,
84
+ enabled: unit.enabled,
85
+ skills: unit.components.skills,
86
+ permissionRisk: unit.permissionRisk
87
+ })).sort((left, right) => left.id.localeCompare(right.id));
88
+ }
89
+
90
+ export function explainSkill(workspace, skillId) {
91
+ const skill = skillById(workspace, skillId);
92
+ const source = classifySkillSource(workspace, skill);
93
+ const workflows = workspace.workflows
94
+ .map((workflow) => workflowSkillRole(workspace, workflow, skillId))
95
+ .filter((entry) => entry.roles.length > 0);
96
+ const capabilities = workspace.capabilities
97
+ .filter((capability) => capability.canonical === skillId || capability.alternatives.includes(skillId))
98
+ .map((capability) => ({
99
+ name: capability.name,
100
+ role: capability.canonical === skillId ? "canonical" : "alternative",
101
+ defaultPolicy: capability.defaultPolicy
102
+ }));
103
+
104
+ return {
105
+ ...skillSummary(workspace, skill),
106
+ source,
107
+ trust: classifySkillTrust(workspace, skill),
108
+ workflows,
109
+ capabilities,
110
+ replacedBy: skill.replacedBy ?? null,
111
+ conflictsWith: skill.conflictsWith
112
+ };
113
+ }
114
+
115
+ export {
116
+ activateSkill,
117
+ addHarness,
118
+ addSkill,
119
+ addWorkflow,
120
+ auditSources,
121
+ blockSkill,
122
+ canUseSkill,
123
+ classifySkillSource,
124
+ classifySkillTrust,
125
+ preferSkill,
126
+ quarantineSkill,
127
+ removeSkill
128
+ };
129
+
130
+ export async function installGuardHook(options) {
131
+ const { plan, script } = await buildGuardHookInstallPlan(options);
132
+ assertGuardHookPlanIsInstallable(plan);
133
+
134
+ await mkdir(dirname(plan.path), { recursive: true });
135
+ await assertNewNonSymlinkPath(plan.path);
136
+ await writeFile(plan.path, script, { encoding: "utf8", flag: "wx", mode: GUARD_HOOK_MODE });
137
+ await assertRegularFile(plan.path);
138
+ await chmod(plan.path, GUARD_HOOK_MODE);
139
+ return { path: plan.path, workflow: plan.workflow, executable: true };
140
+ }
141
+
142
+ async function assertNewNonSymlinkPath(path) {
143
+ const existing = await lstat(path).catch((error) => {
144
+ if (error?.code === "ENOENT") {
145
+ return undefined;
146
+ }
147
+ throw error;
148
+ });
149
+ if (existing !== undefined) {
150
+ throw new Error(`Refusing to overwrite existing hook path: ${path}`);
151
+ }
152
+ }
153
+
154
+ async function assertRegularFile(path) {
155
+ const stats = await lstat(path);
156
+ if (!stats.isFile() || stats.isSymbolicLink()) {
157
+ throw new Error(`Hook path is not a regular file: ${path}`);
158
+ }
159
+ }
160
+
161
+ function workflowByName(workspace, name) {
162
+ const workflow = workspace.workflows.find((candidate) => candidate.name === name);
163
+ if (workflow === undefined) {
164
+ throw new Error(`Unknown workflow: ${name}`);
165
+ }
166
+ return workflow;
167
+ }
168
+
169
+ function skillById(workspace, skillId) {
170
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
171
+ if (skill === undefined) {
172
+ throw new Error(`Unknown skill: ${skillId}`);
173
+ }
174
+ return skill;
175
+ }
176
+
177
+ function workflowSkillRole(workspace, workflow, skillId) {
178
+ const roles = [];
179
+ const capabilityRoles = [];
180
+ if (workflow.activeSkills.includes(skillId)) {
181
+ roles.push("active");
182
+ }
183
+ if (workflow.blockedSkills.includes(skillId)) {
184
+ roles.push("blocked");
185
+ }
186
+ for (const capability of workflow.requiredCapabilities) {
187
+ if (capability.preferred === skillId) {
188
+ roles.push("preferred");
189
+ capabilityRoles.push({ capability: capability.name, role: "preferred", policy: capability.policy });
190
+ }
191
+ if (capability.fallback.includes(skillId)) {
192
+ roles.push("fallback");
193
+ capabilityRoles.push({ capability: capability.name, role: "fallback", policy: capability.policy });
194
+ }
195
+ }
196
+ return { workflow: workflow.name, roles: [...new Set(roles)], capabilityRoles };
197
+ }
package/src/doctor.mjs ADDED
@@ -0,0 +1,299 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { auditSources } from "./control.mjs";
4
+ import { hasRuntimeComponents, installUnitSourceClass, isModelSelectableInvocation } from "./domain/source-classes.mjs";
5
+ import { BRIDGE_END, BRIDGE_START } from "./lifecycle-content.mjs";
6
+ import { checkPolicy } from "./policy.mjs";
7
+ import { verifySources } from "./source-verification.mjs";
8
+ import { uninstallProject } from "./uninstall.mjs";
9
+ import { loadWorkspace } from "./workspace.mjs";
10
+
11
+ export async function doctorProject(options = {}) {
12
+ const root = resolve(options.root ?? ".");
13
+ const configPath = resolveUnderRoot(root, options.configPath ?? "skillboard.config.yaml");
14
+ const skillsRoot = resolveUnderRoot(root, options.skillsRoot ?? "skills");
15
+ const bridges = await bridgeStatuses(root);
16
+ const uninstall = await uninstallProject({
17
+ root,
18
+ dryRun: true,
19
+ removeConfig: false,
20
+ removeEmptyDirs: true
21
+ });
22
+ const configExists = await exists(configPath);
23
+ const base = {
24
+ ok: false,
25
+ strictOk: false,
26
+ reviewRequired: false,
27
+ mode: "not-initialized",
28
+ root,
29
+ configPath,
30
+ skillsRoot,
31
+ initialized: false,
32
+ config: {
33
+ exists: configExists,
34
+ valid: false,
35
+ version: null,
36
+ error: configExists ? null : "skillboard.config.yaml was not found"
37
+ },
38
+ bridges,
39
+ workspace: emptyWorkspaceSummary(),
40
+ policy: { ok: false, errors: [], warnings: [] },
41
+ sources: { checked: false, verified: options.verifySources === true, ok: false, errors: [], warnings: [], blockingWarnings: [], units: [] },
42
+ uninstall,
43
+ recommendations: [],
44
+ reviewSummary: emptyReviewSummary()
45
+ };
46
+
47
+ if (!configExists) {
48
+ return finalizeDoctor(base, ["run skillboard init"]);
49
+ }
50
+
51
+ let workspace;
52
+ if (options.workspace !== undefined) {
53
+ workspace = options.workspace;
54
+ } else {
55
+ try {
56
+ workspace = await loadWorkspace({ configPath, skillsRoot });
57
+ } catch (error) {
58
+ return finalizeDoctor({
59
+ ...base,
60
+ config: {
61
+ exists: true,
62
+ valid: false,
63
+ version: null,
64
+ error: error instanceof Error ? error.message : String(error)
65
+ }
66
+ }, ["fix skillboard.config.yaml, then run skillboard check"]);
67
+ }
68
+ }
69
+
70
+ const policy = checkPolicy(workspace);
71
+ const sourceAudit = options.verifySources === true
72
+ ? await verifySources(workspace, { configPath, rootDir: root, restrictToRoot: true })
73
+ : auditSources(workspace);
74
+ const blockingWarnings = blockingSourceWarnings(sourceAudit.warnings);
75
+ const workspaceSummary = summarizeWorkspace(workspace);
76
+ const result = {
77
+ ...base,
78
+ initialized: true,
79
+ config: {
80
+ exists: true,
81
+ valid: true,
82
+ version: workspace.version,
83
+ error: null
84
+ },
85
+ workspace: workspaceSummary,
86
+ policy,
87
+ sources: {
88
+ checked: true,
89
+ verified: options.verifySources === true,
90
+ ok: sourceAudit.ok,
91
+ errors: sourceAudit.errors,
92
+ warnings: sourceAudit.warnings,
93
+ blockingWarnings,
94
+ units: sourceAudit.units
95
+ }
96
+ };
97
+
98
+ return finalizeDoctor(result, doctorRecommendations(result));
99
+ }
100
+
101
+ function finalizeDoctor(result, recommendations) {
102
+ const bridgeOk = result.bridges.every((bridge) => bridge.status === "installed" || bridge.status === "absent")
103
+ && result.bridges.some((bridge) => bridge.status === "installed");
104
+ const ok = result.config.valid && bridgeOk && result.policy.ok && result.sources.ok;
105
+ const reviewRequired = ok && reviewRequiredFor(result);
106
+ const strictOk = ok && !reviewRequired;
107
+ return {
108
+ ...result,
109
+ ok,
110
+ strictOk,
111
+ reviewRequired,
112
+ mode: ok ? reviewRequired ? "safe-mode" : "passed" : result.initialized ? "failed" : "not-initialized",
113
+ recommendations: [...new Set(recommendations)],
114
+ reviewSummary: reviewSummaryFor(result)
115
+ };
116
+ }
117
+
118
+ function doctorRecommendations(result) {
119
+ const recommendations = [];
120
+ if (!result.bridges.some((bridge) => bridge.status === "installed")) {
121
+ recommendations.push("run skillboard init to install agent bridge blocks");
122
+ }
123
+ if (result.bridges.some((bridge) => bridge.status === "unmanaged")) {
124
+ recommendations.push("run skillboard init to add SkillBoard bridge blocks to unmanaged agent files");
125
+ }
126
+ if (result.bridges.some((bridge) => bridge.status === "broken")) {
127
+ recommendations.push("repair AGENTS.md or CLAUDE.md SkillBoard bridge markers");
128
+ }
129
+ if (!result.policy.ok) {
130
+ recommendations.push("run skillboard check and fix policy errors");
131
+ }
132
+ if (!result.sources.ok) {
133
+ recommendations.push("run skillboard audit sources --verify and fix source verification errors");
134
+ } else if (result.sources.blockingWarnings.length > 0) {
135
+ recommendations.push("review high-risk runtime extension warnings before enabling automatic invocation");
136
+ } else if (result.sources.warnings.length > 0) {
137
+ recommendations.push("review source audit warnings before enabling automatic invocation");
138
+ }
139
+ if (result.workspace.skills.byStatus.quarantined > 0) {
140
+ recommendations.push("review quarantined skills with skillboard explain before activation");
141
+ }
142
+ if (result.workspace.installUnits.highRisk.length > 0) {
143
+ recommendations.push("review high-risk install units before trusting runtime extensions");
144
+ }
145
+ return recommendations;
146
+ }
147
+
148
+ function reviewRequiredFor(result) {
149
+ return result.sources.blockingWarnings.length > 0
150
+ || result.sources.warnings.length > 0
151
+ || result.workspace.skills.byStatus.quarantined > 0
152
+ || result.workspace.installUnits.highRisk.length > 0;
153
+ }
154
+
155
+ function reviewSummaryFor(result) {
156
+ const highRiskReviewUnits = result.sources.units
157
+ .filter((unit) => {
158
+ return unit.permissionRisk === "high" && !["trusted", "reviewed"].includes(unit.trustLevel);
159
+ })
160
+ .map((unit) => unit.id)
161
+ .sort((left, right) => left.localeCompare(right));
162
+ const runtimeReviewUnits = result.sources.units
163
+ .filter((unit) => unit.enabled && unit.trustLevel === "unreviewed" && result.workspace.installUnits.runtimeExtensions.includes(unit.id))
164
+ .map((unit) => unit.id)
165
+ .sort((left, right) => left.localeCompare(right));
166
+ return {
167
+ reviewRequired: reviewRequiredFor(result),
168
+ blockingWarnings: result.sources.blockingWarnings.length,
169
+ warnings: result.sources.warnings.length,
170
+ quarantinedSkills: result.workspace.skills.byStatus.quarantined,
171
+ modelSelectableSkills: result.workspace.skills.modelSelectable,
172
+ highRiskInstallUnits: highRiskReviewUnits,
173
+ runtimeExtensionInstallUnits: runtimeReviewUnits
174
+ };
175
+ }
176
+
177
+ function blockingSourceWarnings(warnings) {
178
+ return warnings.filter((warning) => {
179
+ return warning.includes("high-risk source is not reviewed or trusted")
180
+ || warning.includes("runtime extension source is unreviewed");
181
+ });
182
+ }
183
+
184
+ function summarizeWorkspace(workspace) {
185
+ const skillsByStatus = countBy(workspace.skills, (skill) => skill.status);
186
+ const skillsByInvocation = countBy(workspace.skills, (skill) => skill.invocation);
187
+ const installUnitsByClass = countBy(workspace.installUnits, (unit) => installUnitSourceClass(unit));
188
+ const highRisk = workspace.installUnits
189
+ .filter((unit) => unit.permissionRisk === "high")
190
+ .map((unit) => unit.id)
191
+ .sort((left, right) => left.localeCompare(right));
192
+ const runtimeExtensions = workspace.installUnits
193
+ .filter((unit) => hasRuntimeComponents(unit))
194
+ .map((unit) => unit.id)
195
+ .sort((left, right) => left.localeCompare(right));
196
+ const modelSelectable = workspace.skills
197
+ .filter((skill) => isModelSelectableInvocation(skill.invocation))
198
+ .map((skill) => skill.id)
199
+ .sort((left, right) => left.localeCompare(right));
200
+
201
+ return {
202
+ skills: {
203
+ declared: workspace.skills.length,
204
+ installed: workspace.installedSkills.length,
205
+ modelSelectable: modelSelectable.length,
206
+ byStatus: withKnownKeys(skillsByStatus, ["active", "candidate", "vendor", "quarantined", "blocked", "deprecated"]),
207
+ byInvocation: withKnownKeys(skillsByInvocation, ["manual-only", "router-only", "workflow-auto", "global-auto", "blocked", "deprecated"])
208
+ },
209
+ workflows: workspace.workflows.length,
210
+ harnesses: workspace.harnesses.length,
211
+ installUnits: {
212
+ total: workspace.installUnits.length,
213
+ bySourceClass: withKnownKeys(installUnitsByClass, ["user", "skill-pack", "workflow-bundle", "harness-bundle", "runtime-extension", "package-manager", "external-package", "unknown"]),
214
+ highRisk,
215
+ runtimeExtensions
216
+ }
217
+ };
218
+ }
219
+
220
+ function emptyWorkspaceSummary() {
221
+ return {
222
+ skills: {
223
+ declared: 0,
224
+ installed: 0,
225
+ modelSelectable: 0,
226
+ byStatus: withKnownKeys({}, ["active", "candidate", "vendor", "quarantined", "blocked", "deprecated"]),
227
+ byInvocation: withKnownKeys({}, ["manual-only", "router-only", "workflow-auto", "global-auto", "blocked", "deprecated"])
228
+ },
229
+ workflows: 0,
230
+ harnesses: 0,
231
+ installUnits: {
232
+ total: 0,
233
+ bySourceClass: withKnownKeys({}, ["user", "skill-pack", "workflow-bundle", "harness-bundle", "runtime-extension", "package-manager", "external-package", "unknown"]),
234
+ highRisk: [],
235
+ runtimeExtensions: []
236
+ }
237
+ };
238
+ }
239
+
240
+ function emptyReviewSummary() {
241
+ return {
242
+ reviewRequired: false,
243
+ blockingWarnings: 0,
244
+ warnings: 0,
245
+ quarantinedSkills: 0,
246
+ modelSelectableSkills: 0,
247
+ highRiskInstallUnits: [],
248
+ runtimeExtensionInstallUnits: []
249
+ };
250
+ }
251
+
252
+ async function bridgeStatuses(root) {
253
+ return await Promise.all(["AGENTS.md", "CLAUDE.md"].map(async (filename) => {
254
+ const path = join(root, filename);
255
+ if (!(await exists(path))) {
256
+ return { file: filename, status: "absent" };
257
+ }
258
+ const text = await readFile(path, "utf8");
259
+ const hasStart = text.includes(BRIDGE_START);
260
+ const hasEnd = text.includes(BRIDGE_END);
261
+ if (hasStart && hasEnd) {
262
+ return { file: filename, status: "installed" };
263
+ }
264
+ if (hasStart || hasEnd) {
265
+ return { file: filename, status: "broken" };
266
+ }
267
+ return { file: filename, status: "unmanaged" };
268
+ }));
269
+ }
270
+
271
+ function countBy(values, keyFor) {
272
+ const counts = {};
273
+ for (const value of values) {
274
+ const key = keyFor(value);
275
+ counts[key] = (counts[key] ?? 0) + 1;
276
+ }
277
+ return counts;
278
+ }
279
+
280
+ function withKnownKeys(counts, keys) {
281
+ const next = {};
282
+ for (const key of keys) {
283
+ next[key] = counts[key] ?? 0;
284
+ }
285
+ for (const [key, value] of Object.entries(counts)) {
286
+ if (next[key] === undefined) {
287
+ next[key] = value;
288
+ }
289
+ }
290
+ return next;
291
+ }
292
+
293
+ function resolveUnderRoot(root, path) {
294
+ return isAbsolute(path) ? path : resolve(root, path);
295
+ }
296
+
297
+ async function exists(path) {
298
+ return access(path).then(() => true, () => false);
299
+ }
@@ -0,0 +1,47 @@
1
+ export const STATUS_VALUES = new Set([
2
+ "discovered",
3
+ "quarantined",
4
+ "vendor",
5
+ "candidate",
6
+ "active",
7
+ "active-manual",
8
+ "active-router",
9
+ "active-auto",
10
+ "canonical",
11
+ "blocked",
12
+ "deprecated",
13
+ "archived",
14
+ "removed"
15
+ ]);
16
+
17
+ export const INVOCATION_VALUES = new Set([
18
+ "manual-only",
19
+ "router-only",
20
+ "workflow-auto",
21
+ "global-auto",
22
+ "blocked",
23
+ "deprecated"
24
+ ]);
25
+
26
+ export const HARNESS_STATUS_VALUES = new Set(["available", "configured", "primary", "fallback", "disabled", "removed"]);
27
+ export const EXPOSURE_VALUES = new Set(["exported", "global-meta", "unit-managed", "private"]);
28
+
29
+ export const INSTALL_UNIT_KIND_VALUES = new Set([
30
+ "skill",
31
+ "workflow",
32
+ "plugin",
33
+ "marketplace",
34
+ "package-manager-dependency",
35
+ "harness",
36
+ "mcp-server",
37
+ "hook",
38
+ "agent",
39
+ "lsp",
40
+ "custom"
41
+ ]);
42
+
43
+ export const PERMISSION_RISK_VALUES = new Set(["low", "medium", "high", "unknown"]);
44
+ export const TRUST_LEVEL_VALUES = new Set(["trusted", "reviewed", "unreviewed", "blocked"]);
45
+
46
+ export const NON_CALLABLE_WORKFLOW_STATUSES = new Set(["blocked", "quarantined", "deprecated", "archived", "removed"]);
47
+ export const NON_CALLABLE_WORKFLOW_INVOCATIONS = new Set(["blocked", "deprecated"]);
@@ -0,0 +1,29 @@
1
+ export function buildPolicyContext(workspace) {
2
+ const skillsById = new Map(workspace.skills.map((skill) => [skill.id, skill]));
3
+ const workflowsByName = new Map(workspace.workflows.map((workflow) => [workflow.name, workflow]));
4
+ const harnessesByName = new Map(workspace.harnesses.map((harness) => [harness.name, harness]));
5
+ const installUnitsById = new Map(workspace.installUnits.map((unit) => [unit.id, unit]));
6
+ const capabilitiesByName = new Map(workspace.capabilities.map((capability) => [capability.name, capability]));
7
+ const workflowScopedSkillIds = new Set(workspace.workflows.flatMap((workflow) => {
8
+ const capabilitySkills = workflow.requiredCapabilities.flatMap((capability) => {
9
+ return capability.preferred === "" ? capability.fallback : [capability.preferred, ...capability.fallback];
10
+ });
11
+ return [...workflow.activeSkills, ...capabilitySkills];
12
+ }));
13
+
14
+ return {
15
+ workspace,
16
+ defaults: workspace.defaults,
17
+ skills: workspace.skills,
18
+ workflows: workspace.workflows,
19
+ harnesses: workspace.harnesses,
20
+ installUnits: workspace.installUnits,
21
+ capabilities: workspace.capabilities,
22
+ skillsById,
23
+ workflowsByName,
24
+ harnessesByName,
25
+ installUnitsById,
26
+ capabilitiesByName,
27
+ workflowScopedSkillIds
28
+ };
29
+ }
@@ -0,0 +1,33 @@
1
+ import { INVOCATION_VALUES } from "../constants.mjs";
2
+
3
+ export const capabilityRules = [
4
+ {
5
+ id: "CAP-REF-001",
6
+ check(ctx) {
7
+ const diagnostics = [];
8
+ for (const capability of ctx.capabilities) {
9
+ if (capability.canonical !== "" && !ctx.skillsById.has(capability.canonical)) {
10
+ diagnostics.push(`Capability requirement ${capability.name} references undeclared canonical skill: ${capability.canonical}`);
11
+ }
12
+ for (const skillId of capability.alternatives) {
13
+ if (!ctx.skillsById.has(skillId)) {
14
+ diagnostics.push(`Capability requirement ${capability.name} references undeclared alternative skill: ${skillId}`);
15
+ }
16
+ }
17
+ }
18
+ return diagnostics;
19
+ }
20
+ },
21
+ {
22
+ id: "CAP-POLICY-001",
23
+ check(ctx) {
24
+ const diagnostics = [];
25
+ for (const capability of ctx.capabilities) {
26
+ if (!INVOCATION_VALUES.has(capability.defaultPolicy)) {
27
+ diagnostics.push(`Capability ${capability.name} has unsupported default_policy: ${capability.defaultPolicy}`);
28
+ }
29
+ }
30
+ return diagnostics;
31
+ }
32
+ }
33
+ ];
@@ -0,0 +1,16 @@
1
+ export const harnessRules = [
2
+ {
3
+ id: "HARNESS-REF-001",
4
+ check(ctx) {
5
+ const diagnostics = [];
6
+ for (const harness of ctx.harnesses) {
7
+ for (const workflow of harness.workflows) {
8
+ if (!ctx.workflowsByName.has(workflow)) {
9
+ diagnostics.push(`Harness ${harness.name} references undeclared workflow: ${workflow}`);
10
+ }
11
+ }
12
+ }
13
+ return diagnostics;
14
+ }
15
+ }
16
+ ];