loadout-ai 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/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { compareCatalogPackages } from "./ranking.js";
|
|
2
|
+
/**
|
|
3
|
+
* Stable is Loadout's recommended daily driver: broad enough to improve normal
|
|
4
|
+
* engineering work immediately, but bounded at skill granularity and restricted
|
|
5
|
+
* to catalog sources with an identified SPDX license. Maximum remains the
|
|
6
|
+
* explicit broad-library mode.
|
|
7
|
+
*/
|
|
8
|
+
export const STABLE_SKILL_ALLOWLIST = {
|
|
9
|
+
superpowers: [
|
|
10
|
+
"dispatching-parallel-agents",
|
|
11
|
+
"executing-plans",
|
|
12
|
+
"receiving-code-review",
|
|
13
|
+
"requesting-code-review",
|
|
14
|
+
"test-driven-development",
|
|
15
|
+
"using-superpowers",
|
|
16
|
+
"verification-before-completion",
|
|
17
|
+
"writing-plans",
|
|
18
|
+
],
|
|
19
|
+
context7: ["context7-cli", "context7-mcp", "find-docs"],
|
|
20
|
+
"addyosmani-agent-skills": [
|
|
21
|
+
"api-and-interface-design",
|
|
22
|
+
"code-review-and-quality",
|
|
23
|
+
"context-engineering",
|
|
24
|
+
"debugging-and-error-recovery",
|
|
25
|
+
"deprecation-and-migration",
|
|
26
|
+
"documentation-and-adrs",
|
|
27
|
+
"frontend-ui-engineering",
|
|
28
|
+
"git-workflow-and-versioning",
|
|
29
|
+
"observability-and-instrumentation",
|
|
30
|
+
"performance-optimization",
|
|
31
|
+
"planning-and-task-breakdown",
|
|
32
|
+
"shipping-and-launch",
|
|
33
|
+
"spec-driven-development",
|
|
34
|
+
],
|
|
35
|
+
"wshobson-agents": [
|
|
36
|
+
"architecture-patterns",
|
|
37
|
+
"code-review-excellence",
|
|
38
|
+
"error-handling-patterns",
|
|
39
|
+
"modern-javascript-patterns",
|
|
40
|
+
"postmortem-writing",
|
|
41
|
+
"python-type-safety",
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
export const STABLE_BOOST_PACKAGE_IDS = Object.freeze(Object.keys(STABLE_SKILL_ALLOWLIST));
|
|
45
|
+
/**
|
|
46
|
+
* Trust is deliberately separate from popularity and publisher tier. No
|
|
47
|
+
* bundled record is labelled human-reviewed or benchmarked until that evidence
|
|
48
|
+
* is actually stored; Stable is the policy-recommended subset.
|
|
49
|
+
*/
|
|
50
|
+
export function catalogTrustStage(pkg) {
|
|
51
|
+
if (STABLE_BOOST_PACKAGE_IDS.includes(pkg.id) &&
|
|
52
|
+
pkg.license &&
|
|
53
|
+
pkg.license !== "NOASSERTION" &&
|
|
54
|
+
pkg.source?.commit &&
|
|
55
|
+
!pkg.archived)
|
|
56
|
+
return "recommended";
|
|
57
|
+
if (pkg.source?.commit && pkg.source.evidencePaths.length)
|
|
58
|
+
return "inspected";
|
|
59
|
+
return "discovered";
|
|
60
|
+
}
|
|
61
|
+
export function isStableSkillSelected(packageId, skillName, targetName) {
|
|
62
|
+
const selected = STABLE_SKILL_ALLOWLIST[packageId];
|
|
63
|
+
if (!selected)
|
|
64
|
+
return true;
|
|
65
|
+
return (selected.includes(skillName ?? targetName) || selected.includes(targetName));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Broad daily-driver capabilities selected at skill granularity. Collection
|
|
69
|
+
* repositories stay available in full through Maximum Library, while Power
|
|
70
|
+
* activates only skills with a clear cross-project job.
|
|
71
|
+
*/
|
|
72
|
+
export const POWER_SKILL_ALLOWLIST = {
|
|
73
|
+
superpowers: [
|
|
74
|
+
"brainstorming",
|
|
75
|
+
"dispatching-parallel-agents",
|
|
76
|
+
"executing-plans",
|
|
77
|
+
"finishing-a-development-branch",
|
|
78
|
+
"receiving-code-review",
|
|
79
|
+
"requesting-code-review",
|
|
80
|
+
"subagent-driven-development",
|
|
81
|
+
"systematic-debugging",
|
|
82
|
+
"test-driven-development",
|
|
83
|
+
"using-git-worktrees",
|
|
84
|
+
"using-superpowers",
|
|
85
|
+
"verification-before-completion",
|
|
86
|
+
"writing-plans",
|
|
87
|
+
"writing-skills",
|
|
88
|
+
],
|
|
89
|
+
context7: ["context7-cli", "context7-docs", "context7-mcp", "find-docs"],
|
|
90
|
+
"anthropic-skills": [
|
|
91
|
+
"docx",
|
|
92
|
+
"frontend-design",
|
|
93
|
+
"mcp-builder",
|
|
94
|
+
"pdf",
|
|
95
|
+
"web-artifacts-builder",
|
|
96
|
+
"webapp-testing",
|
|
97
|
+
],
|
|
98
|
+
"openai-skills": [
|
|
99
|
+
"cli-creator",
|
|
100
|
+
"figma-implement-design",
|
|
101
|
+
"gh-fix-ci",
|
|
102
|
+
"imagegen",
|
|
103
|
+
"openai-docs",
|
|
104
|
+
"playwright",
|
|
105
|
+
"screenshot",
|
|
106
|
+
"security-best-practices",
|
|
107
|
+
],
|
|
108
|
+
"vercel-agent-skills": [
|
|
109
|
+
"deploy-to-vercel",
|
|
110
|
+
"vercel-composition-patterns",
|
|
111
|
+
"vercel-optimize",
|
|
112
|
+
"vercel-react-best-practices",
|
|
113
|
+
"web-design-guidelines",
|
|
114
|
+
],
|
|
115
|
+
"ui-ux-pro-max": ["design-system", "slides", "ui-styling", "ui-ux-pro-max"],
|
|
116
|
+
"wshobson-agents": [
|
|
117
|
+
"accessibility-compliance",
|
|
118
|
+
"api-design-principles",
|
|
119
|
+
"architecture-patterns",
|
|
120
|
+
"code-review-excellence",
|
|
121
|
+
"debugging-strategies",
|
|
122
|
+
"e2e-testing-patterns",
|
|
123
|
+
"modern-javascript-patterns",
|
|
124
|
+
"python-testing-patterns",
|
|
125
|
+
"python-type-safety",
|
|
126
|
+
"typescript-advanced-types",
|
|
127
|
+
],
|
|
128
|
+
"awesome-copilot": [
|
|
129
|
+
"acquire-codebase-knowledge",
|
|
130
|
+
"chrome-devtools",
|
|
131
|
+
"create-implementation-plan",
|
|
132
|
+
"github-actions-hardening",
|
|
133
|
+
"security-review",
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
export function isPowerSkillSelected(packageId, skillName, targetName) {
|
|
137
|
+
const selected = POWER_SKILL_ALLOWLIST[packageId];
|
|
138
|
+
if (!selected)
|
|
139
|
+
return false;
|
|
140
|
+
return (selected.includes(skillName ?? targetName) || selected.includes(targetName));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* These are overlap warnings, not claims that the listed repositories are
|
|
144
|
+
* technically incompatible. The catalog currently has no evidenced hard
|
|
145
|
+
* family; hard-family support exists for a future verified incompatibility.
|
|
146
|
+
*/
|
|
147
|
+
export const CATALOG_CONFLICT_FAMILIES = [
|
|
148
|
+
{
|
|
149
|
+
id: "agent-skill-collections",
|
|
150
|
+
label: "agent skill collections",
|
|
151
|
+
severity: "soft",
|
|
152
|
+
packageIds: ["openai-skills", "anthropic-skills", "wshobson-agents"],
|
|
153
|
+
rationale: "Each is a broad skills collection, so installing several can duplicate discovery and workflow guidance.",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "web-research-mcp",
|
|
157
|
+
label: "web research MCP servers",
|
|
158
|
+
severity: "soft",
|
|
159
|
+
packageIds: ["exa-mcp-server", "firecrawl-mcp-server"],
|
|
160
|
+
rationale: "Both expose web search/crawling capabilities; they can coexist but may overlap in cost, permissions, and tool choice.",
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
function eligiblePackages(packages, selection) {
|
|
164
|
+
const ranked = [...packages]
|
|
165
|
+
.filter((pkg) => !pkg.archived)
|
|
166
|
+
.sort(compareCatalogPackages);
|
|
167
|
+
if (selection.mode === "stable") {
|
|
168
|
+
const reviewed = ranked.filter((pkg) => pkg.tier === "official" || pkg.tier === "stable");
|
|
169
|
+
const curated = reviewed.filter((pkg) => STABLE_BOOST_PACKAGE_IDS.includes(pkg.id));
|
|
170
|
+
// Fixtures and downstream catalogs may not contain Loadout's bundled ids;
|
|
171
|
+
// preserve reviewed-tier behavior in that case instead of returning empty.
|
|
172
|
+
return curated.length ? curated : reviewed;
|
|
173
|
+
}
|
|
174
|
+
if (selection.mode === "power") {
|
|
175
|
+
const packageIds = new Set(Object.keys(POWER_SKILL_ALLOWLIST));
|
|
176
|
+
const selected = ranked.filter((pkg) => packageIds.has(pkg.id));
|
|
177
|
+
return selected.length
|
|
178
|
+
? selected
|
|
179
|
+
: ranked.filter((pkg) => pkg.components?.includes("skill"));
|
|
180
|
+
}
|
|
181
|
+
if (selection.mode === "maximum")
|
|
182
|
+
return ranked;
|
|
183
|
+
const ids = selection.packageIds ?? [];
|
|
184
|
+
if (ids.length === 0)
|
|
185
|
+
throw new Error("Custom mode requires at least one package id");
|
|
186
|
+
const byId = new Map(packages.map((pkg) => [pkg.id, pkg]));
|
|
187
|
+
const unknown = ids.filter((id) => !byId.has(id));
|
|
188
|
+
if (unknown.length)
|
|
189
|
+
throw new Error(`Unknown catalog package id(s): ${unknown.join(", ")}`);
|
|
190
|
+
const archived = ids.filter((id) => byId.get(id)?.archived);
|
|
191
|
+
if (archived.length)
|
|
192
|
+
throw new Error(`Archived catalog package(s) cannot be selected: ${archived.join(", ")}`);
|
|
193
|
+
return [...new Set(ids)]
|
|
194
|
+
.map((id) => byId.get(id))
|
|
195
|
+
.sort(compareCatalogPackages);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve catalog overlap before any repository is fetched or files are
|
|
199
|
+
* written. Stable keeps one explainable default per soft family. Maximum and
|
|
200
|
+
* Custom retain soft-overlap candidates but report the chosen default; runtime
|
|
201
|
+
* activation is never claimed because skills do not have a universal enable
|
|
202
|
+
* switch. Hard families always stop the operation for human resolution.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveCatalogProfile(packages, selection, families = CATALOG_CONFLICT_FAMILIES) {
|
|
205
|
+
if (!["stable", "power", "maximum", "custom"].includes(selection.mode))
|
|
206
|
+
throw new Error(`Unknown install mode '${selection.mode}'`);
|
|
207
|
+
const candidates = eligiblePackages(packages, selection);
|
|
208
|
+
const selected = new Map(candidates.map((pkg) => [pkg.id, pkg]));
|
|
209
|
+
const deferred = [];
|
|
210
|
+
const conflicts = [];
|
|
211
|
+
const warnings = [];
|
|
212
|
+
for (const family of families) {
|
|
213
|
+
const members = candidates
|
|
214
|
+
.filter((pkg) => family.packageIds.includes(pkg.id))
|
|
215
|
+
.sort(compareCatalogPackages);
|
|
216
|
+
if (members.length < 2)
|
|
217
|
+
continue;
|
|
218
|
+
const names = members.map((pkg) => pkg.displayName).join(", ");
|
|
219
|
+
if (family.severity === "hard") {
|
|
220
|
+
throw new Error(`Hard conflict in ${family.label}: ${names}. Remove all but one package before continuing. ${family.rationale}`);
|
|
221
|
+
}
|
|
222
|
+
const primary = members[0];
|
|
223
|
+
const message = `${family.label}: ${primary.displayName} is the default among ${names}. ${family.rationale}`;
|
|
224
|
+
conflicts.push({
|
|
225
|
+
familyId: family.id,
|
|
226
|
+
severity: family.severity,
|
|
227
|
+
packageIds: members.map((pkg) => pkg.id),
|
|
228
|
+
defaultPackageId: primary.id,
|
|
229
|
+
message,
|
|
230
|
+
});
|
|
231
|
+
if (selection.mode === "stable") {
|
|
232
|
+
for (const secondary of members.slice(1)) {
|
|
233
|
+
selected.delete(secondary.id);
|
|
234
|
+
deferred.push(secondary);
|
|
235
|
+
}
|
|
236
|
+
warnings.push(`Stable Boost selected ${primary.displayName}; deferred ${members
|
|
237
|
+
.slice(1)
|
|
238
|
+
.map((pkg) => pkg.displayName)
|
|
239
|
+
.join(", ")} because they overlap in ${family.label}.`);
|
|
240
|
+
}
|
|
241
|
+
else if (selection.mode === "custom") {
|
|
242
|
+
warnings.push(`Custom selection retains the soft overlap in ${family.label}. Review ${names} before installation.`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
warnings.push(`${selection.mode === "power" ? "Power Boost" : "Maximum Library"} retains the soft overlap in ${family.label}. ${primary.displayName} is the recommended default; review each package before installation.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
mode: selection.mode,
|
|
250
|
+
packages: [...selected.values()].sort(compareCatalogPackages),
|
|
251
|
+
deferred: deferred.sort(compareCatalogPackages),
|
|
252
|
+
conflicts,
|
|
253
|
+
warnings,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { loadEffectiveCatalog } from "./catalog.js";
|
|
5
|
+
import { ensureDirectory, loadoutHome } from "./paths.js";
|
|
6
|
+
import { discoverSkillDirectories } from "./skills.js";
|
|
7
|
+
import { fetchRepositorySnapshot, } from "./source.js";
|
|
8
|
+
import { writeFileAtomically } from "./atomic-file.js";
|
|
9
|
+
const catalogSkillIndexPath = () => join(loadoutHome(), "provenance", "catalog-skills.json");
|
|
10
|
+
function immutableCatalogDigest(catalog) {
|
|
11
|
+
const evidence = catalog
|
|
12
|
+
.filter((pkg) => pkg.source?.commit && pkg.components?.includes("skill"))
|
|
13
|
+
.map((pkg) => `${pkg.id}\0${pkg.source.commit}`)
|
|
14
|
+
.sort()
|
|
15
|
+
.join("\n");
|
|
16
|
+
return createHash("sha256").update(evidence).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
function cleanFrontmatterValue(value) {
|
|
19
|
+
if (!value)
|
|
20
|
+
return undefined;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
23
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")))
|
|
24
|
+
return trimmed.slice(1, -1);
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
function portablePath(root, path) {
|
|
28
|
+
return relative(resolve(root), resolve(path)).split(sep).join("/") || ".";
|
|
29
|
+
}
|
|
30
|
+
async function parallelMap(values, concurrency, worker) {
|
|
31
|
+
const results = new Array(values.length);
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
async function run() {
|
|
34
|
+
while (cursor < values.length) {
|
|
35
|
+
const index = cursor++;
|
|
36
|
+
results[index] = await worker(values[index]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, concurrency), values.length) }, run));
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
function isCatalogSkillIndex(value) {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
44
|
+
return false;
|
|
45
|
+
const item = value;
|
|
46
|
+
const recordsValid = Array.isArray(item.records) &&
|
|
47
|
+
item.records.every((record) => {
|
|
48
|
+
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
49
|
+
return false;
|
|
50
|
+
const candidate = record;
|
|
51
|
+
return (typeof candidate.packageId === "string" &&
|
|
52
|
+
typeof candidate.packageDisplayName === "string" &&
|
|
53
|
+
typeof candidate.repository === "string" &&
|
|
54
|
+
typeof candidate.commit === "string" &&
|
|
55
|
+
["official", "stable", "trending", "community"].includes(String(candidate.tier)) &&
|
|
56
|
+
typeof candidate.category === "string" &&
|
|
57
|
+
(candidate.license === undefined ||
|
|
58
|
+
typeof candidate.license === "string") &&
|
|
59
|
+
typeof candidate.skillName === "string" &&
|
|
60
|
+
(candidate.description === undefined ||
|
|
61
|
+
typeof candidate.description === "string") &&
|
|
62
|
+
typeof candidate.skillPath === "string" &&
|
|
63
|
+
typeof candidate.fingerprint === "string");
|
|
64
|
+
});
|
|
65
|
+
const failuresValid = Array.isArray(item.failures) &&
|
|
66
|
+
item.failures.every((failure) => {
|
|
67
|
+
if (!failure || typeof failure !== "object" || Array.isArray(failure))
|
|
68
|
+
return false;
|
|
69
|
+
const candidate = failure;
|
|
70
|
+
return (typeof candidate.packageId === "string" &&
|
|
71
|
+
typeof candidate.repository === "string" &&
|
|
72
|
+
typeof candidate.error === "string");
|
|
73
|
+
});
|
|
74
|
+
return (item.schemaVersion === 1 &&
|
|
75
|
+
typeof item.catalogDigest === "string" &&
|
|
76
|
+
typeof item.generatedAt === "string" &&
|
|
77
|
+
recordsValid &&
|
|
78
|
+
failuresValid);
|
|
79
|
+
}
|
|
80
|
+
export async function readCatalogSkillIndex() {
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(await readFile(catalogSkillIndexPath(), "utf8"));
|
|
83
|
+
return isCatalogSkillIndex(value) ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function buildCatalogSkillIndex(options = {}) {
|
|
90
|
+
const catalog = options.catalog ?? (await loadEffectiveCatalog());
|
|
91
|
+
const packages = catalog.filter((pkg) => pkg.source?.commit && pkg.components?.includes("skill"));
|
|
92
|
+
const fetchSnapshot = options.fetchSnapshot ?? fetchRepositorySnapshot;
|
|
93
|
+
let completed = 0;
|
|
94
|
+
const results = await parallelMap(packages, options.concurrency ?? 4, async (pkg) => {
|
|
95
|
+
options.onProgress?.({
|
|
96
|
+
packageId: pkg.id,
|
|
97
|
+
completed,
|
|
98
|
+
total: packages.length,
|
|
99
|
+
status: "fetching",
|
|
100
|
+
message: `Indexing reviewed ${pkg.displayName} skills`,
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
const snapshot = await fetchSnapshot(pkg.repository, {
|
|
104
|
+
ref: pkg.source.commit,
|
|
105
|
+
});
|
|
106
|
+
if (snapshot.commit.toLowerCase() !== pkg.source.commit.toLowerCase())
|
|
107
|
+
throw new Error(`resolved ${snapshot.commit}, expected ${pkg.source.commit}`);
|
|
108
|
+
const directories = await discoverSkillDirectories(snapshot.path);
|
|
109
|
+
const records = await Promise.all(directories.map(async (directory) => {
|
|
110
|
+
const content = await readFile(join(directory, "SKILL.md"), "utf8");
|
|
111
|
+
const description = cleanFrontmatterValue(content.match(/^description:\s*(.+)$/m)?.[1]);
|
|
112
|
+
return {
|
|
113
|
+
packageId: pkg.id,
|
|
114
|
+
packageDisplayName: pkg.displayName,
|
|
115
|
+
repository: pkg.repository,
|
|
116
|
+
commit: pkg.source.commit,
|
|
117
|
+
tier: pkg.tier,
|
|
118
|
+
category: pkg.category,
|
|
119
|
+
...(pkg.license ? { license: pkg.license } : {}),
|
|
120
|
+
skillName: cleanFrontmatterValue(content.match(/^name:\s*(.+)$/m)?.[1]) ??
|
|
121
|
+
directory.split(sep).at(-1) ??
|
|
122
|
+
"unnamed",
|
|
123
|
+
...(description ? { description } : {}),
|
|
124
|
+
skillPath: portablePath(snapshot.path, directory),
|
|
125
|
+
fingerprint: createHash("sha256").update(content).digest("hex"),
|
|
126
|
+
};
|
|
127
|
+
}));
|
|
128
|
+
completed += 1;
|
|
129
|
+
options.onProgress?.({
|
|
130
|
+
packageId: pkg.id,
|
|
131
|
+
completed,
|
|
132
|
+
total: packages.length,
|
|
133
|
+
status: "ready",
|
|
134
|
+
message: `${pkg.displayName}: ${records.length} reviewed skill(s) indexed`,
|
|
135
|
+
});
|
|
136
|
+
return { records };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
completed += 1;
|
|
140
|
+
const failure = {
|
|
141
|
+
packageId: pkg.id,
|
|
142
|
+
repository: pkg.repository,
|
|
143
|
+
error: error instanceof Error ? error.message : String(error),
|
|
144
|
+
};
|
|
145
|
+
options.onProgress?.({
|
|
146
|
+
packageId: pkg.id,
|
|
147
|
+
completed,
|
|
148
|
+
total: packages.length,
|
|
149
|
+
status: "failed",
|
|
150
|
+
message: `${pkg.displayName}: ${failure.error}`,
|
|
151
|
+
});
|
|
152
|
+
return { records: [], failure };
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const index = {
|
|
156
|
+
schemaVersion: 1,
|
|
157
|
+
catalogDigest: immutableCatalogDigest(catalog),
|
|
158
|
+
generatedAt: (options.now ?? new Date()).toISOString(),
|
|
159
|
+
records: results
|
|
160
|
+
.flatMap((result) => result.records)
|
|
161
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId) ||
|
|
162
|
+
left.skillPath.localeCompare(right.skillPath)),
|
|
163
|
+
failures: results
|
|
164
|
+
.flatMap((result) => (result.failure ? [result.failure] : []))
|
|
165
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId)),
|
|
166
|
+
};
|
|
167
|
+
await ensureDirectory(dirname(catalogSkillIndexPath()));
|
|
168
|
+
await writeFileAtomically(catalogSkillIndexPath(), `${JSON.stringify(index, null, 2)}\n`);
|
|
169
|
+
return index;
|
|
170
|
+
}
|
|
171
|
+
export async function resolveCatalogSkillIndex(options = {}) {
|
|
172
|
+
const catalog = options.build?.catalog ?? (await loadEffectiveCatalog());
|
|
173
|
+
const digest = immutableCatalogDigest(catalog);
|
|
174
|
+
const cached = await readCatalogSkillIndex();
|
|
175
|
+
const stale = Boolean(cached && cached.catalogDigest !== digest);
|
|
176
|
+
if (!options.refresh && cached && !stale)
|
|
177
|
+
return { index: cached, source: "cache" };
|
|
178
|
+
if (options.offline)
|
|
179
|
+
return cached
|
|
180
|
+
? { index: cached, source: stale ? "stale-cache" : "cache" }
|
|
181
|
+
: { source: "none" };
|
|
182
|
+
return {
|
|
183
|
+
index: await buildCatalogSkillIndex({
|
|
184
|
+
...options.build,
|
|
185
|
+
catalog,
|
|
186
|
+
}),
|
|
187
|
+
source: "refreshed",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function candidatesFor(skill, index) {
|
|
191
|
+
const records = index?.records ?? [];
|
|
192
|
+
if (skill.managed) {
|
|
193
|
+
const candidates = records.filter((record) => record.packageId === skill.packageId);
|
|
194
|
+
return {
|
|
195
|
+
kind: "loadout-managed",
|
|
196
|
+
confidence: "high",
|
|
197
|
+
evidence: [
|
|
198
|
+
`Loadout state owns this path as package '${skill.packageId}'.`,
|
|
199
|
+
...(candidates.length
|
|
200
|
+
? ["The managed package also exists in the reviewed catalog index."]
|
|
201
|
+
: []),
|
|
202
|
+
],
|
|
203
|
+
candidates,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const exact = records.filter((record) => record.fingerprint === skill.fingerprint);
|
|
207
|
+
if (exact.length)
|
|
208
|
+
return {
|
|
209
|
+
kind: "catalog-exact",
|
|
210
|
+
confidence: "exact",
|
|
211
|
+
evidence: [
|
|
212
|
+
`SKILL.md SHA-256 exactly matches ${exact.length} reviewed catalog record(s).`,
|
|
213
|
+
],
|
|
214
|
+
candidates: exact,
|
|
215
|
+
};
|
|
216
|
+
const hints = new Set((skill.sourceHints ?? []).map((item) => item.toLowerCase()));
|
|
217
|
+
const embedded = records.filter((record) => hints.has(record.repository.toLowerCase()));
|
|
218
|
+
if (embedded.length)
|
|
219
|
+
return {
|
|
220
|
+
kind: "embedded-source",
|
|
221
|
+
confidence: "medium",
|
|
222
|
+
evidence: [
|
|
223
|
+
"The skill text names a reviewed repository, but its instruction fingerprint differs from the reviewed commit.",
|
|
224
|
+
],
|
|
225
|
+
candidates: embedded,
|
|
226
|
+
};
|
|
227
|
+
const name = skill.name.trim().toLowerCase();
|
|
228
|
+
const named = records.filter((record) => record.skillName.trim().toLowerCase() === name);
|
|
229
|
+
if (named.length)
|
|
230
|
+
return {
|
|
231
|
+
kind: "catalog-name-candidate",
|
|
232
|
+
confidence: "low",
|
|
233
|
+
evidence: [
|
|
234
|
+
"Only the normalized skill name matches; names alone do not establish provenance or equivalence.",
|
|
235
|
+
],
|
|
236
|
+
candidates: named,
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
kind: "unknown",
|
|
240
|
+
confidence: "unknown",
|
|
241
|
+
evidence: [
|
|
242
|
+
index
|
|
243
|
+
? "No managed record, exact fingerprint, embedded reviewed source, or reviewed name match was found."
|
|
244
|
+
: "No catalog skill index is available; run with provenance refresh for reviewed matching.",
|
|
245
|
+
],
|
|
246
|
+
candidates: [],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function enrichInventoryWithProvenance(report, index, source = "none") {
|
|
250
|
+
const skills = report.skills.map((skill) => ({
|
|
251
|
+
...skill,
|
|
252
|
+
provenance: candidatesFor(skill, index),
|
|
253
|
+
}));
|
|
254
|
+
const count = (kind) => skills.filter((skill) => skill.provenance.kind === kind).length;
|
|
255
|
+
return {
|
|
256
|
+
...report,
|
|
257
|
+
skills,
|
|
258
|
+
provenance: {
|
|
259
|
+
indexSource: source,
|
|
260
|
+
...(index ? { catalogDigest: index.catalogDigest } : {}),
|
|
261
|
+
...(index ? { indexGeneratedAt: index.generatedAt } : {}),
|
|
262
|
+
indexedSkills: index?.records.length ?? 0,
|
|
263
|
+
exact: count("catalog-exact"),
|
|
264
|
+
managed: count("loadout-managed"),
|
|
265
|
+
embedded: count("embedded-source"),
|
|
266
|
+
nameCandidates: count("catalog-name-candidate"),
|
|
267
|
+
unknown: count("unknown"),
|
|
268
|
+
failures: index?.failures ?? [],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function formatProvenanceSummary(report) {
|
|
273
|
+
const value = report.provenance;
|
|
274
|
+
return [
|
|
275
|
+
`Provenance: ${value.exact} exact catalog match(es), ${value.managed} Loadout-managed, ${value.embedded} embedded-source candidate(s), ${value.nameCandidates} name-only candidate(s), ${value.unknown} unknown`,
|
|
276
|
+
`Catalog skill index: ${value.indexSource}; ${value.indexedSkills} reviewed skill(s)${value.failures.length ? `; ${value.failures.length} repository failure(s)` : ""}`,
|
|
277
|
+
].join("\n");
|
|
278
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const TIER_ORDER = {
|
|
2
|
+
official: 4,
|
|
3
|
+
stable: 3,
|
|
4
|
+
trending: 2,
|
|
5
|
+
community: 1,
|
|
6
|
+
};
|
|
7
|
+
function bounded(value, maximum) {
|
|
8
|
+
return Math.max(0, Math.min(maximum, value));
|
|
9
|
+
}
|
|
10
|
+
function daysSince(value, now) {
|
|
11
|
+
const timestamp = Date.parse(value);
|
|
12
|
+
if (Number.isNaN(timestamp) || timestamp > now.getTime())
|
|
13
|
+
return undefined;
|
|
14
|
+
return (now.getTime() - timestamp) / 86_400_000;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Explain the score from evidence that is actually present in the catalog.
|
|
18
|
+
* A single star count cannot establish momentum, so momentum is intentionally
|
|
19
|
+
* assigned zero points until the discovery service stores a second observation.
|
|
20
|
+
*/
|
|
21
|
+
export function explainCatalogScore(pkg, now = new Date()) {
|
|
22
|
+
const adoption = pkg.stars === undefined
|
|
23
|
+
? {
|
|
24
|
+
factor: "adoption",
|
|
25
|
+
points: 0,
|
|
26
|
+
maximum: 30,
|
|
27
|
+
evidence: "No independently fetched adoption count is available.",
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
factor: "adoption",
|
|
31
|
+
points: Number(bounded((Math.log10(pkg.stars + 1) / 6) * 30, 30).toFixed(2)),
|
|
32
|
+
maximum: 30,
|
|
33
|
+
evidence: `${pkg.stars.toLocaleString()} GitHub stars, logarithmically scaled and capped.`,
|
|
34
|
+
};
|
|
35
|
+
const momentum = {
|
|
36
|
+
factor: "momentum",
|
|
37
|
+
points: 0,
|
|
38
|
+
maximum: 20,
|
|
39
|
+
evidence: "No two-point star or download history is stored yet; a single snapshot is not treated as momentum.",
|
|
40
|
+
};
|
|
41
|
+
const age = pkg.pushedAt ? daysSince(pkg.pushedAt, now) : undefined;
|
|
42
|
+
const maintenance = age === undefined
|
|
43
|
+
? {
|
|
44
|
+
factor: "maintenance",
|
|
45
|
+
points: 0,
|
|
46
|
+
maximum: 20,
|
|
47
|
+
evidence: "No valid code-push timestamp is available; metadata edits and catalog verification are not counted as maintenance.",
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
factor: "maintenance",
|
|
51
|
+
points: Number((age <= 30
|
|
52
|
+
? 20
|
|
53
|
+
: age <= 90
|
|
54
|
+
? 16
|
|
55
|
+
: age <= 180
|
|
56
|
+
? 10
|
|
57
|
+
: age <= 365
|
|
58
|
+
? 5
|
|
59
|
+
: 0).toFixed(2)),
|
|
60
|
+
maximum: 20,
|
|
61
|
+
evidence: `Repository code was last pushed ${Math.floor(age)} day(s) ago.`,
|
|
62
|
+
};
|
|
63
|
+
const platforms = pkg.operatingSystems?.length ?? 0;
|
|
64
|
+
// Catalog operatingSystems describe where Loadout can inspect the Git
|
|
65
|
+
// source, not where every component has been proven installable. Until a
|
|
66
|
+
// record carries agent-specific compatibility evidence, do not award those
|
|
67
|
+
// ten points.
|
|
68
|
+
const compatibilityPoints = pkg.components?.length ? 5 : 0;
|
|
69
|
+
const compatibility = {
|
|
70
|
+
factor: "compatibility",
|
|
71
|
+
points: Number(compatibilityPoints.toFixed(2)),
|
|
72
|
+
maximum: 15,
|
|
73
|
+
evidence: `${pkg.components?.length ?? 0} evidenced component kind(s) and ${platforms} source-inspection platform(s); no package or adapter compatibility is inferred.`,
|
|
74
|
+
};
|
|
75
|
+
const assertedLicense = Boolean(pkg.license && pkg.license.toUpperCase() !== "NOASSERTION");
|
|
76
|
+
const trustPoints = (pkg.source ? 6 : 0) +
|
|
77
|
+
(assertedLicense ? 4 : 0) +
|
|
78
|
+
(pkg.tier === "official" ? 5 : 0);
|
|
79
|
+
const trust = {
|
|
80
|
+
factor: "trust",
|
|
81
|
+
points: trustPoints,
|
|
82
|
+
maximum: 15,
|
|
83
|
+
evidence: `${pkg.source ? "Immutable source evidence" : "No immutable source evidence"}, ${assertedLicense ? `asserted ${pkg.license} license metadata` : pkg.license === "NOASSERTION" ? "license is unknown (NOASSERTION)" : "no license metadata"}${pkg.tier === "official" ? ", and a declared official publisher tier" : ""}.`,
|
|
84
|
+
};
|
|
85
|
+
const contributions = [adoption, momentum, maintenance, compatibility, trust];
|
|
86
|
+
return {
|
|
87
|
+
score: Number(contributions
|
|
88
|
+
.reduce((sum, contribution) => sum + contribution.points, 0)
|
|
89
|
+
.toFixed(2)),
|
|
90
|
+
contributions,
|
|
91
|
+
guardrails: [
|
|
92
|
+
"Scores order candidates inside a capability category; they do not compare unrelated tools or prove universal quality.",
|
|
93
|
+
"Stars are logarithmic and capped, so popularity cannot overwhelm tier, provenance, license, archive, or compatibility policy.",
|
|
94
|
+
"Missing evidence earns no points. Self-reported README claims, topics, and a single timestamp are not treated as adoption, momentum, or safety proof.",
|
|
95
|
+
"Official is a publisher-identity tier, not a security guarantee. Archived packages remain ineligible for automatic profiles.",
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** A deterministic ordering: review tier first, then explainable evidence, then id. */
|
|
100
|
+
export function compareCatalogPackages(a, b) {
|
|
101
|
+
return (TIER_ORDER[b.tier] - TIER_ORDER[a.tier] ||
|
|
102
|
+
explainCatalogScore(b).score - explainCatalogScore(a).score ||
|
|
103
|
+
a.id.localeCompare(b.id));
|
|
104
|
+
}
|