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.
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/bin/skillboard.mjs +4 -0
- package/docs/adapters.md +127 -0
- package/docs/capabilities.md +107 -0
- package/docs/install.md +346 -0
- package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
- package/docs/plans/README.md +20 -0
- package/docs/policy-model.md +251 -0
- package/docs/positioning.md +94 -0
- package/docs/profiles.md +166 -0
- package/docs/rollout-runbook.md +60 -0
- package/docs/user-flow.md +231 -0
- package/docs/versioning.md +201 -0
- package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
- package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
- package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
- package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
- package/examples/multi-source.config.yaml +271 -0
- package/examples/skillboard.config.yaml +194 -0
- package/examples/skills/grill-me/SKILL.md +9 -0
- package/examples/skills/grill-with-docs/SKILL.md +9 -0
- package/examples/skills/requirement-intake/SKILL.md +9 -0
- package/examples/skills/tdd/SKILL.md +8 -0
- package/package.json +58 -0
- package/profiles/anthropics-skills.yaml +17 -0
- package/profiles/mattpocock-skills.yaml +26 -0
- package/profiles/oh-my-openagent.yaml +32 -0
- package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
- package/profiles/wshobson-agents.yaml +19 -0
- package/src/advisor/action-core.mjs +74 -0
- package/src/advisor/actions.mjs +256 -0
- package/src/advisor/application-commands.mjs +59 -0
- package/src/advisor/apply-action.mjs +183 -0
- package/src/advisor/schema.mjs +112 -0
- package/src/advisor/setup-actions.mjs +42 -0
- package/src/advisor/skills.mjs +191 -0
- package/src/advisor/sort.mjs +15 -0
- package/src/advisor/sources.mjs +160 -0
- package/src/advisor/trust-policy.mjs +65 -0
- package/src/advisor/workflow.mjs +41 -0
- package/src/advisor.mjs +134 -0
- package/src/agent-inventory-platforms.mjs +100 -0
- package/src/agent-inventory.mjs +804 -0
- package/src/brief-cli.mjs +40 -0
- package/src/brief-renderer.mjs +362 -0
- package/src/change-plan.mjs +169 -0
- package/src/cli.mjs +1171 -0
- package/src/config-helpers.mjs +72 -0
- package/src/control/can-use-guard.mjs +91 -0
- package/src/control/config-write.mjs +163 -0
- package/src/control/skill-crud.mjs +394 -0
- package/src/control/source-trust.mjs +161 -0
- package/src/control/workflow-crud.mjs +104 -0
- package/src/control.mjs +197 -0
- package/src/doctor.mjs +299 -0
- package/src/domain/constants.mjs +47 -0
- package/src/domain/indexes.mjs +29 -0
- package/src/domain/rules/capabilities.mjs +33 -0
- package/src/domain/rules/harnesses.mjs +16 -0
- package/src/domain/rules/install-units.mjs +95 -0
- package/src/domain/rules/skills.mjs +105 -0
- package/src/domain/rules/workflows.mjs +105 -0
- package/src/domain/skill-state-matrix.mjs +79 -0
- package/src/domain/source-classes.mjs +99 -0
- package/src/hook-plan.mjs +152 -0
- package/src/impact.mjs +41 -0
- package/src/index.mjs +82 -0
- package/src/init.mjs +136 -0
- package/src/install-output-detector.mjs +337 -0
- package/src/install-units.mjs +62 -0
- package/src/inventory-refresh.mjs +45 -0
- package/src/lifecycle-cli.mjs +166 -0
- package/src/lifecycle-content.mjs +87 -0
- package/src/policy.mjs +42 -0
- package/src/reconcile.mjs +111 -0
- package/src/report.mjs +151 -0
- package/src/review.mjs +88 -0
- package/src/rollout.mjs +453 -0
- package/src/skill-paths.mjs +17 -0
- package/src/source-cache.mjs +178 -0
- package/src/source-profile-loader.mjs +160 -0
- package/src/source-profiles.mjs +299 -0
- package/src/source-verification.mjs +252 -0
- package/src/uninstall.mjs +258 -0
- package/src/workspace.mjs +219 -0
- package/tsconfig.lsp.json +15 -0
package/src/control.mjs
ADDED
|
@@ -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
|
+
];
|