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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/MASTER_PLAN.md +1711 -0
  3. package/README.md +453 -0
  4. package/SECURITY.md +18 -0
  5. package/SIMPLE_PLAN.md +44 -0
  6. package/catalog/discovered.json +10469 -0
  7. package/catalog/packages.json +1150 -0
  8. package/dashboard/app.js +607 -0
  9. package/dashboard/index.html +249 -0
  10. package/dashboard/styles.css +384 -0
  11. package/dist/src/cli.js +2735 -0
  12. package/dist/src/core/active-policy.js +233 -0
  13. package/dist/src/core/active-set.js +382 -0
  14. package/dist/src/core/adapters.js +254 -0
  15. package/dist/src/core/adopt.js +92 -0
  16. package/dist/src/core/agent-health-score.js +475 -0
  17. package/dist/src/core/agent-inspection.js +152 -0
  18. package/dist/src/core/agent-versions.js +131 -0
  19. package/dist/src/core/api.js +90 -0
  20. package/dist/src/core/atomic-file.js +22 -0
  21. package/dist/src/core/audit.js +174 -0
  22. package/dist/src/core/benchmark-campaign.js +496 -0
  23. package/dist/src/core/benchmark-evidence.js +458 -0
  24. package/dist/src/core/benchmark-fixtures.js +1157 -0
  25. package/dist/src/core/benchmark-runner.js +301 -0
  26. package/dist/src/core/benchmark-trust.js +795 -0
  27. package/dist/src/core/canary.js +77 -0
  28. package/dist/src/core/candidate-intelligence.js +661 -0
  29. package/dist/src/core/catalog-coverage.js +85 -0
  30. package/dist/src/core/catalog-install.js +186 -0
  31. package/dist/src/core/catalog-release.js +286 -0
  32. package/dist/src/core/catalog.js +327 -0
  33. package/dist/src/core/codex-mcp.js +122 -0
  34. package/dist/src/core/community.js +86 -0
  35. package/dist/src/core/compatibility-intelligence.js +465 -0
  36. package/dist/src/core/completion.js +160 -0
  37. package/dist/src/core/components.js +138 -0
  38. package/dist/src/core/conversion.js +56 -0
  39. package/dist/src/core/credentials.js +377 -0
  40. package/dist/src/core/demo.js +136 -0
  41. package/dist/src/core/diff.js +83 -0
  42. package/dist/src/core/discovery-connector.js +165 -0
  43. package/dist/src/core/doctor.js +95 -0
  44. package/dist/src/core/ecosystem-import.js +1072 -0
  45. package/dist/src/core/evaluate.js +72 -0
  46. package/dist/src/core/file-lock.js +140 -0
  47. package/dist/src/core/freshness-alerts.js +230 -0
  48. package/dist/src/core/github-discovery.js +97 -0
  49. package/dist/src/core/github.js +148 -0
  50. package/dist/src/core/head-to-head.js +437 -0
  51. package/dist/src/core/health-score-evidence.js +169 -0
  52. package/dist/src/core/health.js +129 -0
  53. package/dist/src/core/improve.js +223 -0
  54. package/dist/src/core/install.js +233 -0
  55. package/dist/src/core/intelligence-feed-build.js +59 -0
  56. package/dist/src/core/intelligence-feed.js +353 -0
  57. package/dist/src/core/loadout-badge.js +55 -0
  58. package/dist/src/core/loadout-card.js +87 -0
  59. package/dist/src/core/manifest.js +307 -0
  60. package/dist/src/core/mcp-recipes.js +419 -0
  61. package/dist/src/core/mcp-registry-discovery.js +455 -0
  62. package/dist/src/core/mcp.js +287 -0
  63. package/dist/src/core/model-config.js +102 -0
  64. package/dist/src/core/observations.js +87 -0
  65. package/dist/src/core/outcomes.js +164 -0
  66. package/dist/src/core/package.js +225 -0
  67. package/dist/src/core/paths.js +214 -0
  68. package/dist/src/core/portable.js +130 -0
  69. package/dist/src/core/private-discovery.js +45 -0
  70. package/dist/src/core/profiles.js +255 -0
  71. package/dist/src/core/provenance.js +278 -0
  72. package/dist/src/core/ranking.js +104 -0
  73. package/dist/src/core/recommend.js +181 -0
  74. package/dist/src/core/registry-api.js +97 -0
  75. package/dist/src/core/registry.js +316 -0
  76. package/dist/src/core/release-claims.js +175 -0
  77. package/dist/src/core/remove.js +122 -0
  78. package/dist/src/core/review-queue.js +200 -0
  79. package/dist/src/core/runtime-tool-recipe.js +315 -0
  80. package/dist/src/core/runtime-tools.js +453 -0
  81. package/dist/src/core/safety.js +234 -0
  82. package/dist/src/core/sandbox.js +78 -0
  83. package/dist/src/core/scheduler.js +264 -0
  84. package/dist/src/core/share-report.js +85 -0
  85. package/dist/src/core/signing.js +115 -0
  86. package/dist/src/core/skill-compare.js +239 -0
  87. package/dist/src/core/skill-inventory.js +193 -0
  88. package/dist/src/core/skill-security.js +340 -0
  89. package/dist/src/core/skills-sh-discovery.js +336 -0
  90. package/dist/src/core/skills.js +208 -0
  91. package/dist/src/core/snapshot.js +210 -0
  92. package/dist/src/core/source.js +305 -0
  93. package/dist/src/core/state.js +300 -0
  94. package/dist/src/core/sync.js +348 -0
  95. package/dist/src/core/terminal.js +53 -0
  96. package/dist/src/core/transaction.js +211 -0
  97. package/dist/src/core/update-watch.js +39 -0
  98. package/dist/src/core/update.js +216 -0
  99. package/dist/src/core/upgrade.js +207 -0
  100. package/dist/src/dashboard.js +418 -0
  101. package/dist/src/shared/schemas.js +336 -0
  102. package/dist/src/shared/types.js +1 -0
  103. package/docs/ACTIVE_SET.md +53 -0
  104. package/docs/ACTIVE_SET_POLICY.md +45 -0
  105. package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
  106. package/docs/CATALOG.md +78 -0
  107. package/docs/CATALOG_POLICY.md +41 -0
  108. package/docs/COMMUNITY_DISCOVERY.md +14 -0
  109. package/docs/COMPATIBILITY_POLICY.md +22 -0
  110. package/docs/CONVERSION_AND_SANDBOX.md +27 -0
  111. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
  112. package/docs/DISCOVERED.md +280 -0
  113. package/docs/EVALUATION_PROTOCOL.md +40 -0
  114. package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
  115. package/docs/FEATURE_TEST_MATRIX.md +802 -0
  116. package/docs/GITHUB_AUTHORIZATION.md +38 -0
  117. package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
  118. package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
  119. package/docs/PROVIDER_CONFIGURATION.md +45 -0
  120. package/docs/RELEASE_REVIEW.md +117 -0
  121. package/docs/SAFE_UPDATE_DEMO.md +25 -0
  122. package/docs/SCHEMA_DECISIONS.md +25 -0
  123. package/docs/TEAM_POLICY.md +18 -0
  124. package/docs/TESTING.md +190 -0
  125. package/package.json +82 -0
@@ -0,0 +1,661 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import { writeFileAtomically } from "./atomic-file.js";
8
+ import { loadEffectiveCatalog, validateCatalog } from "./catalog.js";
9
+ import { evaluatePackage } from "./evaluate.js";
10
+ import { inspectPackage } from "./package.js";
11
+ import { ensureDirectory, loadoutHome } from "./paths.js";
12
+ import { fetchRepositorySnapshot, normalizeRepository, } from "./source.js";
13
+ import { safeTerminalText } from "./terminal.js";
14
+ const dossierDirectory = () => join(loadoutHome(), "candidates");
15
+ const sourceVerifiedDossiers = new WeakMap();
16
+ function installabilityFor(components) {
17
+ if (components.some((component) => ["skill", "rule", "command", "agent"].includes(component)))
18
+ return "portable-components";
19
+ if (components.some((component) => component === "mcp" || component === "plugin"))
20
+ return "explicit-runtime-setup";
21
+ return "unsupported-source-shape";
22
+ }
23
+ function dossierIntegrity(dossier) {
24
+ return createHash("sha256").update(JSON.stringify(dossier)).digest("hex");
25
+ }
26
+ function bundledDiscoveryPath() {
27
+ const moduleDirectory = dirname(fileURLToPath(import.meta.url));
28
+ const candidates = [
29
+ join(moduleDirectory, "..", "..", "catalog", "discovered.json"),
30
+ join(moduleDirectory, "..", "..", "..", "catalog", "discovered.json"),
31
+ join(process.cwd(), "catalog", "discovered.json"),
32
+ ];
33
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
34
+ }
35
+ function isTextArray(value) {
36
+ return (Array.isArray(value) && value.every((item) => typeof item === "string"));
37
+ }
38
+ function finiteNonnegative(value) {
39
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
40
+ }
41
+ function validDate(value) {
42
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
43
+ }
44
+ function parseDiscovery(value) {
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ throw new Error("Discovery artifact must be an object");
47
+ const artifact = value;
48
+ if (artifact.schemaVersion !== 1 ||
49
+ typeof artifact.generatedAt !== "string" ||
50
+ Number.isNaN(Date.parse(artifact.generatedAt)) ||
51
+ !Array.isArray(artifact.repositories))
52
+ throw new Error("Discovery artifact schema is invalid");
53
+ for (const [index, item] of artifact.repositories.entries()) {
54
+ if (!item ||
55
+ typeof item !== "object" ||
56
+ typeof item.repository !== "string" ||
57
+ typeof item.url !== "string" ||
58
+ typeof item.description !== "string" ||
59
+ !finiteNonnegative(item.stars) ||
60
+ !finiteNonnegative(item.forks) ||
61
+ !finiteNonnegative(item.openIssues) ||
62
+ (item.language !== null && typeof item.language !== "string") ||
63
+ typeof item.license !== "string" ||
64
+ !item.license.trim() ||
65
+ !isTextArray(item.topics) ||
66
+ typeof item.defaultBranch !== "string" ||
67
+ !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(item.defaultBranch) ||
68
+ item.defaultBranch.includes("..") ||
69
+ item.defaultBranch.endsWith("/") ||
70
+ !isTextArray(item.matchedQueries) ||
71
+ !validDate(item.createdAt) ||
72
+ !validDate(item.pushedAt) ||
73
+ !validDate(item.updatedAt) ||
74
+ !validDate(item.firstSeenAt) ||
75
+ !validDate(item.lastSeenAt) ||
76
+ typeof item.seenInLatestRun !== "boolean" ||
77
+ (item.starVelocityPerDay !== undefined &&
78
+ (typeof item.starVelocityPerDay !== "number" ||
79
+ !Number.isFinite(item.starVelocityPerDay))) ||
80
+ (item.starVelocityWindowDays !== undefined &&
81
+ (!finiteNonnegative(item.starVelocityWindowDays) ||
82
+ item.starVelocityWindowDays < 1)) ||
83
+ (item.starVelocityPerDay !== undefined) !==
84
+ (item.starVelocityWindowDays !== undefined) ||
85
+ (item.starsPerDaySinceCreation !== undefined &&
86
+ !finiteNonnegative(item.starsPerDaySinceCreation)) ||
87
+ (item.catalogStatus !== "candidate" && item.catalogStatus !== "reviewed"))
88
+ throw new Error(`Discovery repository ${index + 1} is invalid`);
89
+ normalizeRepository(item.repository);
90
+ if (item.url !== `https://github.com/${item.repository}`)
91
+ throw new Error(`Discovery repository ${index + 1} has a mismatched URL`);
92
+ }
93
+ return artifact;
94
+ }
95
+ export async function readDiscoveryArtifact(path = bundledDiscoveryPath()) {
96
+ const target = resolve(path);
97
+ const info = await stat(target);
98
+ if (!info.isFile() || info.size > 10 * 1024 * 1024)
99
+ throw new Error("Discovery artifact exceeds the 10 MiB limit");
100
+ return parseDiscovery(JSON.parse(await readFile(target, "utf8")));
101
+ }
102
+ function summarize(item) {
103
+ const measured = item.starVelocityPerDay !== undefined;
104
+ const starsPerDay = measured
105
+ ? item.starVelocityPerDay
106
+ : (item.starsPerDaySinceCreation ?? 0);
107
+ const queryPoints = Math.min(30, item.matchedQueries.length * 6);
108
+ const growthPoints = Math.min(35, Math.log10(Math.max(0, starsPerDay) + 1) * 14);
109
+ const adoptionPoints = Math.min(25, Math.log10(item.stars + 1) * 5);
110
+ const freshnessPoints = item.seenInLatestRun ? 10 : 0;
111
+ const triagePriority = Math.round((queryPoints + growthPoints + adoptionPoints + freshnessPoints) * 10) / 10;
112
+ return {
113
+ repository: item.repository,
114
+ url: item.url,
115
+ description: item.description,
116
+ stars: item.stars,
117
+ license: item.license,
118
+ matchedQueries: item.matchedQueries,
119
+ seenInLatestRun: item.seenInLatestRun,
120
+ catalogStatus: item.catalogStatus,
121
+ growth: {
122
+ kind: measured ? "observed-star-velocity" : "lifetime-star-average",
123
+ starsPerDay,
124
+ ...(measured && item.starVelocityWindowDays !== undefined
125
+ ? { windowDays: item.starVelocityWindowDays }
126
+ : {}),
127
+ },
128
+ triagePriority,
129
+ triageEvidence: [
130
+ `${item.matchedQueries.length} bounded discovery query match(es)`,
131
+ measured
132
+ ? `${starsPerDay.toFixed(2)} observed stars/day over ${item.starVelocityWindowDays?.toFixed(1) ?? "unknown"} day(s)`
133
+ : `${starsPerDay.toFixed(2)} stars/day lifetime average; not observed velocity`,
134
+ `${item.stars.toLocaleString("en-US")} stars are adoption evidence, not quality or safety evidence`,
135
+ item.seenInLatestRun
136
+ ? "seen in the latest discovery run"
137
+ : "retained from an earlier discovery run",
138
+ ],
139
+ };
140
+ }
141
+ export async function listDiscoveryCandidates(options = {}) {
142
+ const artifact = await readDiscoveryArtifact(options.path);
143
+ const words = (options.query ?? "")
144
+ .toLowerCase()
145
+ .split(/\s+/)
146
+ .filter(Boolean);
147
+ return artifact.repositories
148
+ .filter((item) => options.includeReviewed || item.catalogStatus === "candidate")
149
+ .filter((item) => {
150
+ const haystack = [
151
+ item.repository,
152
+ item.description,
153
+ item.language ?? "",
154
+ ...item.topics,
155
+ ...item.matchedQueries,
156
+ ]
157
+ .join(" ")
158
+ .toLowerCase();
159
+ return words.every((word) => haystack.includes(word));
160
+ })
161
+ .map(summarize)
162
+ .sort((left, right) => right.triagePriority - left.triagePriority ||
163
+ left.repository.localeCompare(right.repository))
164
+ .slice(0, options.limit ?? 20);
165
+ }
166
+ function componentsFor(inspection) {
167
+ const result = [];
168
+ if (inspection.counts.skills)
169
+ result.push("skill");
170
+ if (inspection.counts.rules)
171
+ result.push("rule");
172
+ if (inspection.counts.commands)
173
+ result.push("command");
174
+ if (inspection.counts.agents)
175
+ result.push("agent");
176
+ if (inspection.plugins.some((plugin) => !plugin.warnings.some((warning) => warning.startsWith("invalid plugin manifest"))))
177
+ result.push("plugin");
178
+ if (inspection.mcpServers.length)
179
+ result.push("mcp");
180
+ return result;
181
+ }
182
+ function evidencePathsFor(inspection) {
183
+ return [
184
+ ...inspection.skills.map((item) => join(item.path, "SKILL.md")),
185
+ ...inspection.resources.map((item) => item.path),
186
+ ...inspection.plugins
187
+ .filter((item) => !item.warnings.some((warning) => warning.startsWith("invalid plugin manifest")))
188
+ .map((item) => item.path),
189
+ ...inspection.mcpServers.map((item) => item.path),
190
+ ]
191
+ .map((path) => path.split(/[\\/]/).join("/"))
192
+ .filter((path) => path !== "." && !path.startsWith("../"))
193
+ .filter((path, index, paths) => paths.indexOf(path) === index)
194
+ .sort();
195
+ }
196
+ const STOP_WORDS = new Set([
197
+ "the",
198
+ "and",
199
+ "for",
200
+ "with",
201
+ "from",
202
+ "that",
203
+ "this",
204
+ "your",
205
+ "into",
206
+ "agent",
207
+ "agents",
208
+ "skill",
209
+ "skills",
210
+ "mcp",
211
+ "server",
212
+ "servers",
213
+ "workflow",
214
+ "workflows",
215
+ "code",
216
+ "coding",
217
+ "developer",
218
+ "development",
219
+ "platform",
220
+ "context",
221
+ "open",
222
+ "source",
223
+ "using",
224
+ "across",
225
+ "tool",
226
+ "tools",
227
+ "github",
228
+ ]);
229
+ function tokens(...values) {
230
+ return new Set(values
231
+ .join(" ")
232
+ .toLowerCase()
233
+ .split(/[^a-z0-9]+/)
234
+ .filter((word) => word.length >= 3 && !STOP_WORDS.has(word)));
235
+ }
236
+ function overlapFor(discovery, inspection, catalog) {
237
+ const candidateTokens = tokens(discovery.repository, discovery.description, discovery.topics.join(" "), ...inspection.skills.flatMap((item) => [item.name, item.description]), ...inspection.resources.map((item) => item.name), ...inspection.plugins.flatMap((item) => [item.name, item.description]), ...inspection.mcpServers.map((item) => item.name));
238
+ const candidateComponents = new Set(componentsFor(inspection));
239
+ return catalog
240
+ .map((pkg) => {
241
+ const packageTokens = tokens(pkg.id, pkg.displayName, pkg.description, pkg.category, pkg.topics?.join(" "));
242
+ const common = [...candidateTokens]
243
+ .filter((word) => packageTokens.has(word))
244
+ .sort();
245
+ // Containment is more useful than union similarity here: a large skill
246
+ // collection can overlap a focused package even when most collection
247
+ // tokens describe unrelated capabilities.
248
+ const smallerVocabulary = Math.min(candidateTokens.size, packageTokens.size);
249
+ const score = smallerVocabulary ? common.length / smallerVocabulary : 0;
250
+ const componentMatch = pkg.components?.some((component) => candidateComponents.has(component)) ?? false;
251
+ return {
252
+ packageId: pkg.id,
253
+ repository: pkg.repository,
254
+ score: Math.round(score * 1000) / 1000,
255
+ relationship: componentMatch && common.length >= 2 && score >= 0.2
256
+ ? "possible-overlap"
257
+ : "same-tooling-area",
258
+ evidence: common.slice(0, 8).map((word) => `shared term: ${word}`),
259
+ };
260
+ })
261
+ .filter((item) => item.evidence.length >= 1 && item.score >= 0.1)
262
+ .sort((left, right) => right.score - left.score ||
263
+ left.packageId.localeCompare(right.packageId))
264
+ .slice(0, 5);
265
+ }
266
+ export async function buildCandidateDossier(repository, options = {}) {
267
+ const normalized = normalizeRepository(repository);
268
+ const artifact = await readDiscoveryArtifact(options.discoveryPath);
269
+ const discovery = artifact.repositories.find((item) => item.repository.toLowerCase() === normalized.toLowerCase());
270
+ if (!discovery)
271
+ throw new Error(`${normalized} is not present in the discovery evidence feed; run daily discovery or inspect it separately first`);
272
+ if (discovery.catalogStatus === "reviewed")
273
+ throw new Error(`${discovery.repository} is already in the reviewed catalog`);
274
+ const catalog = options.catalog ?? (await loadEffectiveCatalog());
275
+ if (catalog.some((item) => item.repository.toLowerCase() === discovery.repository.toLowerCase()))
276
+ throw new Error(`${discovery.repository} is already in the effective reviewed catalog; the discovery feed is stale`);
277
+ const snapshot = options.fetchSnapshot
278
+ ? await options.fetchSnapshot(discovery.repository, discovery.defaultBranch)
279
+ : await fetchRepositorySnapshot(discovery.repository, {
280
+ ref: discovery.defaultBranch,
281
+ timeoutMs: 120_000,
282
+ maxBytes: 100 * 1024 * 1024,
283
+ maxFiles: 20_000,
284
+ });
285
+ if (normalizeRepository(snapshot.repository).toLowerCase() !==
286
+ discovery.repository.toLowerCase() ||
287
+ !/^[a-f0-9]{40}$/i.test(snapshot.commit))
288
+ throw new Error("Repository snapshot returned mismatched or non-immutable evidence");
289
+ const [inspection, evaluation] = await Promise.all([
290
+ inspectPackage(snapshot.path),
291
+ evaluatePackage(snapshot.path),
292
+ ]);
293
+ const components = componentsFor(inspection);
294
+ const installability = installabilityFor(components);
295
+ const evidencePaths = evidencePathsFor(inspection);
296
+ const summary = summarize(discovery);
297
+ const blockedCategories = evaluation.categories.filter((item) => item.status === "blocked");
298
+ const reviewFindings = evaluation.categories.flatMap((item) => item.status === "needs-review"
299
+ ? item.findings.map((finding) => `${item.category}: ${finding}`)
300
+ : []);
301
+ const reasons = [
302
+ ...(!components.length
303
+ ? [
304
+ "No portable skill, rule, command, agent, plugin, or MCP declaration was found; this may be a runtime tool that needs an explicit reviewed recipe",
305
+ ]
306
+ : []),
307
+ ...(installability === "explicit-runtime-setup"
308
+ ? [
309
+ "Only plugin/MCP runtime evidence was found; Loadout will not run third-party setup automatically",
310
+ ]
311
+ : []),
312
+ ...(!evidencePaths.length
313
+ ? ["No immutable component evidence paths were found"]
314
+ : []),
315
+ ...blockedCategories.map((item) => `${item.category} static evaluation is blocked`),
316
+ ...reviewFindings,
317
+ ...(discovery.license === "NOASSERTION"
318
+ ? ["GitHub reports NOASSERTION; a human license decision is required"]
319
+ : []),
320
+ "A human must review usefulness, overlap, license, platform claims, and runtime behavior",
321
+ ];
322
+ const dossier = {
323
+ schemaVersion: 1,
324
+ dossierVersion: 1,
325
+ createdAt: (options.now ?? new Date()).toISOString(),
326
+ discoveryGeneratedAt: artifact.generatedAt,
327
+ repository: discovery.repository,
328
+ url: discovery.url,
329
+ commit: snapshot.commit,
330
+ defaultBranch: discovery.defaultBranch,
331
+ description: discovery.description,
332
+ license: discovery.license,
333
+ stars: discovery.stars,
334
+ matchedQueries: discovery.matchedQueries,
335
+ growth: summary.growth,
336
+ triagePriority: summary.triagePriority,
337
+ triageEvidence: summary.triageEvidence,
338
+ inspection: {
339
+ skills: inspection.skills,
340
+ resources: inspection.resources,
341
+ plugins: inspection.plugins,
342
+ mcpServers: inspection.mcpServers,
343
+ counts: inspection.counts,
344
+ warnings: inspection.warnings,
345
+ },
346
+ evaluation: {
347
+ evaluatorVersion: evaluation.evaluatorVersion,
348
+ categories: evaluation.categories,
349
+ uncertainty: evaluation.uncertainty,
350
+ },
351
+ components,
352
+ installability,
353
+ evidencePaths,
354
+ overlap: overlapFor(discovery, inspection, catalog),
355
+ review: {
356
+ status: !components.length || !evidencePaths.length || blockedCategories.length
357
+ ? "blocked"
358
+ : "needs-human-review",
359
+ reasons,
360
+ },
361
+ safetyBoundary: "Static inspection only: Loadout cloned source without running repository scripts, hooks, MCP servers, lifecycle commands, or models.",
362
+ };
363
+ sourceVerifiedDossiers.set(dossier, dossierIntegrity(dossier));
364
+ return dossier;
365
+ }
366
+ export async function writeCandidateDossier(dossier, output) {
367
+ const target = resolve(output ??
368
+ join(dossierDirectory(), `${dossier.repository.replace("/", "__")}.json`));
369
+ await ensureDirectory(dirname(target));
370
+ await writeFileAtomically(target, `${JSON.stringify(dossier, null, 2)}\n`);
371
+ return target;
372
+ }
373
+ function validPersistedEvaluation(value) {
374
+ if (!value || typeof value !== "object" || Array.isArray(value))
375
+ return false;
376
+ const evaluation = value;
377
+ const categories = evaluation.categories;
378
+ return (evaluation.evaluatorVersion === 1 &&
379
+ typeof evaluation.uncertainty === "string" &&
380
+ Array.isArray(categories) &&
381
+ categories.length === 2 &&
382
+ new Set(categories.map((item) => item.category)).size === 2 &&
383
+ categories.every((item) => (item.category === "skills" || item.category === "mcp") &&
384
+ ["ready", "needs-review", "blocked", "not-applicable"].includes(item.status) &&
385
+ isTextArray(item.findings)));
386
+ }
387
+ function safeEvidencePath(value) {
388
+ return (typeof value === "string" &&
389
+ Boolean(value) &&
390
+ !value.startsWith("/") &&
391
+ !value.split(/[\\/]/).includes(".."));
392
+ }
393
+ function validPersistedInspection(value) {
394
+ if (!value || typeof value !== "object" || Array.isArray(value))
395
+ return false;
396
+ const inspection = value;
397
+ const validCount = (count) => typeof count === "number" && Number.isSafeInteger(count) && count >= 0;
398
+ const validNamedPath = (item, type) => Boolean(item &&
399
+ typeof item === "object" &&
400
+ !Array.isArray(item) &&
401
+ item.type === type &&
402
+ typeof item.name === "string" &&
403
+ safeEvidencePath(item.path));
404
+ if (!Array.isArray(inspection.skills) ||
405
+ !inspection.skills.every((item) => validNamedPath(item, "skill") &&
406
+ (item.description === undefined ||
407
+ typeof item.description === "string")) ||
408
+ !Array.isArray(inspection.resources) ||
409
+ !inspection.resources.every((item) => ["rule", "command", "agent"].includes(item.type) &&
410
+ validNamedPath(item, item.type)) ||
411
+ !Array.isArray(inspection.plugins) ||
412
+ !inspection.plugins.every((item) => validNamedPath(item, "plugin") &&
413
+ (item.description === undefined ||
414
+ typeof item.description === "string") &&
415
+ (item.version === undefined || typeof item.version === "string") &&
416
+ (item.author === undefined || typeof item.author === "string") &&
417
+ Array.isArray(item.components) &&
418
+ item.components.every((component) => [
419
+ "skill",
420
+ "rule",
421
+ "command",
422
+ "agent",
423
+ "mcp",
424
+ "plugin",
425
+ "root",
426
+ ].includes(component)) &&
427
+ isTextArray(item.hookEvents) &&
428
+ isTextArray(item.mcpServers) &&
429
+ isTextArray(item.warnings)) ||
430
+ !Array.isArray(inspection.mcpServers) ||
431
+ !inspection.mcpServers.every((item) => validNamedPath(item, "mcp") &&
432
+ ["command", "url", "unknown"].includes(item.transport) &&
433
+ (item.command === undefined || typeof item.command === "string") &&
434
+ (item.url === undefined || typeof item.url === "string") &&
435
+ validCount(item.argumentCount) &&
436
+ validCount(item.environmentVariableCount) &&
437
+ isTextArray(item.warnings)) ||
438
+ !inspection.counts ||
439
+ !validCount(inspection.counts.skills) ||
440
+ !validCount(inspection.counts.rules) ||
441
+ !validCount(inspection.counts.commands) ||
442
+ !validCount(inspection.counts.agents) ||
443
+ !validCount(inspection.counts.plugins) ||
444
+ !validCount(inspection.counts.mcpServers) ||
445
+ !validCount(inspection.counts.manifests) ||
446
+ !isTextArray(inspection.warnings))
447
+ return false;
448
+ return (inspection.counts.skills === inspection.skills.length &&
449
+ inspection.counts.rules ===
450
+ inspection.resources.filter((item) => item.type === "rule").length &&
451
+ inspection.counts.commands ===
452
+ inspection.resources.filter((item) => item.type === "command").length &&
453
+ inspection.counts.agents ===
454
+ inspection.resources.filter((item) => item.type === "agent").length &&
455
+ inspection.counts.plugins === inspection.plugins.length &&
456
+ inspection.counts.mcpServers === inspection.mcpServers.length);
457
+ }
458
+ function assertDossierEvidence(dossier) {
459
+ if (!validPersistedInspection(dossier.inspection))
460
+ throw new Error("Candidate dossier inspection evidence is invalid");
461
+ const inspection = { root: ".", ...dossier.inspection };
462
+ const components = componentsFor(inspection);
463
+ const evidencePaths = evidencePathsFor(inspection);
464
+ if (JSON.stringify(dossier.components) !== JSON.stringify(components))
465
+ throw new Error("Candidate dossier components do not match inspection evidence");
466
+ if (dossier.installability !== installabilityFor(components))
467
+ throw new Error("Candidate dossier installability does not match inspection evidence");
468
+ if (JSON.stringify(dossier.evidencePaths) !== JSON.stringify(evidencePaths))
469
+ throw new Error("Candidate dossier evidencePaths do not match inspection evidence");
470
+ }
471
+ export async function readCandidateDossier(path) {
472
+ const target = resolve(path);
473
+ const info = await stat(target);
474
+ if (!info.isFile() || info.size > 20 * 1024 * 1024)
475
+ throw new Error("Candidate dossier exceeds the 20 MiB limit");
476
+ const value = JSON.parse(await readFile(target, "utf8"));
477
+ if (!value || typeof value !== "object" || Array.isArray(value))
478
+ throw new Error("Candidate dossier must be an object");
479
+ const dossier = value;
480
+ const componentKinds = new Set([
481
+ "skill",
482
+ "rule",
483
+ "command",
484
+ "agent",
485
+ "mcp",
486
+ "plugin",
487
+ "root",
488
+ ]);
489
+ if (dossier.schemaVersion !== 1 ||
490
+ dossier.dossierVersion !== 1 ||
491
+ typeof dossier.repository !== "string" ||
492
+ typeof dossier.url !== "string" ||
493
+ dossier.url !== `https://github.com/${dossier.repository}` ||
494
+ typeof dossier.commit !== "string" ||
495
+ !/^[a-f0-9]{40}$/i.test(dossier.commit) ||
496
+ typeof dossier.defaultBranch !== "string" ||
497
+ !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(dossier.defaultBranch) ||
498
+ dossier.defaultBranch.includes("..") ||
499
+ typeof dossier.description !== "string" ||
500
+ typeof dossier.license !== "string" ||
501
+ !finiteNonnegative(dossier.stars) ||
502
+ !validDate(dossier.createdAt) ||
503
+ !validDate(dossier.discoveryGeneratedAt) ||
504
+ !isTextArray(dossier.matchedQueries) ||
505
+ !Array.isArray(dossier.components) ||
506
+ dossier.components.some((component) => !componentKinds.has(component)) ||
507
+ new Set(dossier.components).size !== dossier.components.length ||
508
+ ![
509
+ "portable-components",
510
+ "explicit-runtime-setup",
511
+ "unsupported-source-shape",
512
+ ].includes(dossier.installability ?? "") ||
513
+ !Array.isArray(dossier.evidencePaths) ||
514
+ dossier.evidencePaths.some((item) => typeof item !== "string" ||
515
+ !item ||
516
+ item.startsWith("/") ||
517
+ item.split("/").includes("..")) ||
518
+ !dossier.growth ||
519
+ (dossier.growth.kind !== "observed-star-velocity" &&
520
+ dossier.growth.kind !== "lifetime-star-average") ||
521
+ typeof dossier.growth.starsPerDay !== "number" ||
522
+ !Number.isFinite(dossier.growth.starsPerDay) ||
523
+ !finiteNonnegative(dossier.triagePriority) ||
524
+ !isTextArray(dossier.triageEvidence) ||
525
+ !validPersistedInspection(dossier.inspection) ||
526
+ !validPersistedEvaluation(dossier.evaluation) ||
527
+ !Array.isArray(dossier.overlap) ||
528
+ !dossier.review ||
529
+ (dossier.review.status !== "blocked" &&
530
+ dossier.review.status !== "needs-human-review") ||
531
+ !isTextArray(dossier.review.reasons) ||
532
+ typeof dossier.safetyBoundary !== "string")
533
+ throw new Error("Candidate dossier schema is invalid");
534
+ normalizeRepository(dossier.repository);
535
+ if (dossier.overlap.some((item) => !item ||
536
+ typeof item !== "object" ||
537
+ typeof item.packageId !== "string" ||
538
+ typeof item.repository !== "string" ||
539
+ typeof item.score !== "number" ||
540
+ !Number.isFinite(item.score) ||
541
+ item.score < 0 ||
542
+ item.score > 1 ||
543
+ (item.relationship !== "possible-overlap" &&
544
+ item.relationship !== "same-tooling-area") ||
545
+ !isTextArray(item.evidence)))
546
+ throw new Error("Candidate dossier overlap evidence is invalid");
547
+ for (const item of dossier.overlap)
548
+ normalizeRepository(item.repository);
549
+ assertDossierEvidence(dossier);
550
+ const derivedStatus = dossier.components.length === 0 ||
551
+ dossier.evidencePaths.length === 0 ||
552
+ dossier.evaluation.categories.some((item) => item.status === "blocked")
553
+ ? "blocked"
554
+ : "needs-human-review";
555
+ if (dossier.review.status !== derivedStatus)
556
+ throw new Error(`Candidate dossier review status is inconsistent; expected ${derivedStatus}`);
557
+ return dossier;
558
+ }
559
+ /** Recompute static evidence from the exact pinned commit before admission. */
560
+ export async function verifyCandidateDossierSource(dossier, options = {}) {
561
+ const snapshot = options.fetchSnapshot
562
+ ? await options.fetchSnapshot(dossier.repository, dossier.commit)
563
+ : await fetchRepositorySnapshot(dossier.repository, {
564
+ ref: dossier.commit,
565
+ timeoutMs: 120_000,
566
+ maxBytes: 100 * 1024 * 1024,
567
+ maxFiles: 20_000,
568
+ });
569
+ if (normalizeRepository(snapshot.repository).toLowerCase() !==
570
+ dossier.repository.toLowerCase() ||
571
+ snapshot.commit.toLowerCase() !== dossier.commit.toLowerCase())
572
+ throw new Error("Candidate dossier source verification returned a mismatch");
573
+ const [inspection, evaluation] = await Promise.all([
574
+ inspectPackage(snapshot.path),
575
+ evaluatePackage(snapshot.path),
576
+ ]);
577
+ const persistedInspection = {
578
+ skills: inspection.skills,
579
+ resources: inspection.resources,
580
+ plugins: inspection.plugins,
581
+ mcpServers: inspection.mcpServers,
582
+ counts: inspection.counts,
583
+ warnings: inspection.warnings,
584
+ };
585
+ const persistedEvaluation = {
586
+ evaluatorVersion: evaluation.evaluatorVersion,
587
+ categories: evaluation.categories,
588
+ uncertainty: evaluation.uncertainty,
589
+ };
590
+ if (!isDeepStrictEqual(dossier.inspection, persistedInspection) ||
591
+ !isDeepStrictEqual(dossier.evaluation, persistedEvaluation))
592
+ throw new Error("Candidate dossier static evidence differs from its pinned source; inspect it again");
593
+ sourceVerifiedDossiers.set(dossier, dossierIntegrity(dossier));
594
+ return dossier;
595
+ }
596
+ export function buildCatalogProposal(dossier, options, existingCatalog = []) {
597
+ assertDossierEvidence(dossier);
598
+ if (!dossier.components.length ||
599
+ !dossier.evidencePaths.length ||
600
+ dossier.evaluation.categories.some((item) => item.status === "blocked"))
601
+ throw new Error("Blocked dossier evidence cannot become a catalog proposal");
602
+ if (dossier.review.status === "blocked")
603
+ throw new Error("Blocked dossiers cannot become catalog proposals");
604
+ if (dossier.review.status !== "needs-human-review")
605
+ throw new Error("Only human-review-ready dossiers can become catalog proposals");
606
+ if (sourceVerifiedDossiers.get(dossier) !== dossierIntegrity(dossier))
607
+ throw new Error("Candidate dossier must be re-verified against its pinned source before proposal");
608
+ if (!/^[a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?$/.test(options.id))
609
+ throw new Error("Proposal id must be lowercase kebab-case");
610
+ if (!options.category.trim())
611
+ throw new Error("Proposal category is required");
612
+ if (!options.operatingSystems.length)
613
+ throw new Error("At least one explicitly reviewed platform is required");
614
+ if (existingCatalog.some((item) => item.id === options.id))
615
+ throw new Error(`Catalog id '${options.id}' is already reviewed`);
616
+ if (existingCatalog.some((item) => item.repository.toLowerCase() === dossier.repository.toLowerCase()))
617
+ throw new Error(`${dossier.repository} is already in the reviewed catalog`);
618
+ const proposal = {
619
+ id: options.id,
620
+ displayName: options.displayName?.trim() || dossier.repository.split("/")[1],
621
+ repository: dossier.repository,
622
+ description: options.description?.trim() || dossier.description,
623
+ category: options.category.trim(),
624
+ tier: options.tier ?? "community",
625
+ license: options.license?.trim() || dossier.license,
626
+ components: dossier.components,
627
+ operatingSystems: [...new Set(options.operatingSystems)],
628
+ source: {
629
+ type: "github",
630
+ url: dossier.url,
631
+ defaultBranch: dossier.defaultBranch,
632
+ commit: dossier.commit,
633
+ evidencePaths: dossier.evidencePaths,
634
+ verifiedAt: dossier.createdAt,
635
+ },
636
+ stars: dossier.stars,
637
+ };
638
+ validateCatalog([proposal], { requireEvidence: true });
639
+ return proposal;
640
+ }
641
+ export function formatCandidateSummaries(candidates) {
642
+ if (!candidates.length)
643
+ return "No matching discovery candidates.";
644
+ return candidates
645
+ .map((item) => `${safeTerminalText(item.repository)} — priority ${item.triagePriority.toFixed(1)} — ★${item.stars.toLocaleString("en-US")} — ${item.growth.starsPerDay.toFixed(2)} stars/day (${item.growth.kind}) — ${safeTerminalText(item.license)}`)
646
+ .join("\n");
647
+ }
648
+ export function formatCandidateDossier(dossier) {
649
+ const counts = dossier.inspection.counts;
650
+ return [
651
+ `${safeTerminalText(dossier.repository)} @ ${dossier.commit}`,
652
+ `Review: ${dossier.review.status}`,
653
+ `Installability: ${dossier.installability}`,
654
+ `Evidence: ${dossier.components.join(", ") || "none"}; ${dossier.evidencePaths.length} path(s)`,
655
+ `Contents: ${counts.skills} skills, ${counts.rules} rules, ${counts.commands} commands, ${counts.agents} agents, ${counts.plugins} plugins, ${counts.mcpServers} MCP servers`,
656
+ `Triage priority: ${dossier.triagePriority.toFixed(1)} (discovery ordering, not a quality score)`,
657
+ `Possible overlaps: ${dossier.overlap.map((item) => `${item.packageId} ${item.score.toFixed(3)}`).join(", ") || "none detected"}`,
658
+ ...dossier.review.reasons.map((reason) => ` - ${safeTerminalText(reason)}`),
659
+ safeTerminalText(dossier.safetyBoundary),
660
+ ].join("\n");
661
+ }