loadout-ai 0.6.0 → 0.8.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 (133) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +129 -26
  3. package/catalog/discovered.json +28469 -24524
  4. package/dist/src/cli.js +7 -2
  5. package/dist/src/commands/agents.js +6 -23
  6. package/dist/src/commands/catalog.js +186 -102
  7. package/dist/src/commands/health.js +6 -6
  8. package/dist/src/commands/inventory.js +19 -120
  9. package/dist/src/commands/lifecycle.js +13 -51
  10. package/dist/src/commands/mcp.js +10 -73
  11. package/dist/src/commands/setup.js +5 -5
  12. package/dist/src/commands/sharing.js +11 -108
  13. package/dist/src/commands/support.js +20 -9
  14. package/dist/src/core/{adapters.js → agents/adapters.js} +2 -2
  15. package/dist/src/core/{agent-inspection.js → agents/agent-inspection.js} +48 -6
  16. package/dist/src/core/{codex-mcp.js → agents/codex-mcp.js} +1 -1
  17. package/dist/src/core/{health-score-evidence.js → agents/health-score-evidence.js} +8 -8
  18. package/dist/src/core/{runtime-tools.js → agents/runtime-tools.js} +2 -2
  19. package/dist/src/core/{audit.js → catalog/audit.js} +4 -4
  20. package/dist/src/core/{catalog.js → catalog/catalog.js} +5 -5
  21. package/dist/src/core/{components.js → catalog/components.js} +2 -2
  22. package/dist/src/core/{conformance.js → catalog/conformance.js} +2 -2
  23. package/dist/src/core/{profiles.js → catalog/profiles.js} +2 -2
  24. package/dist/src/core/{provenance.js → catalog/provenance.js} +3 -3
  25. package/dist/src/core/{registry.js → catalog/registry.js} +59 -11
  26. package/dist/src/core/{safety.js → catalog/safety.js} +36 -7
  27. package/dist/src/core/{skill-compare.js → catalog/skill-compare.js} +1 -1
  28. package/dist/src/core/{skill-inventory.js → catalog/skill-inventory.js} +3 -3
  29. package/dist/src/core/{skills.js → catalog/skills.js} +1 -1
  30. package/dist/src/core/{candidate-intelligence.js → discovery/candidate-intelligence.js} +6 -6
  31. package/dist/src/core/{discovery-connector.js → discovery/discovery-connector.js} +2 -2
  32. package/dist/src/core/{evaluate.js → discovery/evaluate.js} +2 -2
  33. package/dist/src/core/{mcp-registry-discovery.js → discovery/mcp-registry-discovery.js} +1 -1
  34. package/dist/src/core/{observations.js → discovery/observations.js} +3 -3
  35. package/dist/src/core/{review-queue.js → discovery/review-queue.js} +2 -2
  36. package/dist/src/core/{skills-sh-discovery.js → discovery/skills-sh-discovery.js} +1 -1
  37. package/dist/src/core/{adopt.js → install/adopt.js} +3 -3
  38. package/dist/src/core/{catalog-install.js → install/catalog-install.js} +11 -11
  39. package/dist/src/core/{file-lock.js → install/file-lock.js} +1 -1
  40. package/dist/src/core/{install.js → install/install.js} +5 -5
  41. package/dist/src/core/{package.js → install/package.js} +3 -3
  42. package/dist/src/core/{reconcile.js → install/reconcile.js} +5 -5
  43. package/dist/src/core/{remove.js → install/remove.js} +4 -4
  44. package/dist/src/core/{snapshot.js → install/snapshot.js} +1 -1
  45. package/dist/src/core/{source.js → install/source.js} +22 -8
  46. package/dist/src/core/{sync.js → install/sync.js} +9 -9
  47. package/dist/src/core/{transaction.js → install/transaction.js} +1 -1
  48. package/dist/src/core/{uninstall.js → install/uninstall.js} +4 -4
  49. package/dist/src/core/{update.js → install/update.js} +4 -4
  50. package/dist/src/core/{cli-guide.js → reporting/cli-guide.js} +29 -45
  51. package/dist/src/core/reporting/completion.js +90 -0
  52. package/dist/src/core/{doctor.js → reporting/doctor.js} +5 -10
  53. package/dist/src/core/{freshness-alerts.js → reporting/freshness-alerts.js} +5 -5
  54. package/dist/src/core/{health.js → reporting/health.js} +6 -6
  55. package/dist/src/core/{loadout-card.js → reporting/loadout-card.js} +1 -1
  56. package/dist/src/core/{readme-claims.js → reporting/readme-claims.js} +1 -1
  57. package/dist/src/core/{readme-facts.js → reporting/readme-facts.js} +2 -2
  58. package/dist/src/core/{share-report.js → reporting/share-report.js} +3 -3
  59. package/dist/src/core/{upgrade.js → reporting/upgrade.js} +7 -7
  60. package/dist/src/core/routing/first-party-skills.js +118 -0
  61. package/dist/src/core/routing/handoff.js +314 -0
  62. package/dist/src/core/{model-config.js → routing/model-config.js} +4 -4
  63. package/dist/src/core/routing/policy.js +147 -0
  64. package/dist/src/core/{route.js → routing/route.js} +202 -39
  65. package/dist/src/core/{api.js → runtime/api.js} +4 -4
  66. package/dist/src/core/{canary.js → runtime/canary.js} +1 -1
  67. package/dist/src/core/{github.js → runtime/github.js} +1 -1
  68. package/dist/src/core/{mcp-recipes.js → runtime/mcp-recipes.js} +1 -1
  69. package/dist/src/core/{mcp.js → runtime/mcp.js} +2 -2
  70. package/dist/src/core/{scheduler.js → runtime/scheduler.js} +4 -4
  71. package/dist/src/core/{active-policy.js → workspace/active-policy.js} +2 -2
  72. package/dist/src/core/{active-set.js → workspace/active-set.js} +2 -2
  73. package/dist/src/core/{improve.js → workspace/improve.js} +3 -3
  74. package/dist/src/core/{manifest.js → workspace/manifest.js} +2 -2
  75. package/dist/src/core/{outcomes.js → workspace/outcomes.js} +3 -3
  76. package/dist/src/core/{portable.js → workspace/portable.js} +3 -3
  77. package/dist/src/core/{profile-state.js → workspace/profile-state.js} +1 -1
  78. package/dist/src/core/{recommend.js → workspace/recommend.js} +1 -1
  79. package/dist/src/core/{state.js → workspace/state.js} +3 -3
  80. package/docs/CANDIDATE_INTELLIGENCE.md +9 -2
  81. package/docs/CATALOG.md +1 -1
  82. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +1 -1
  83. package/docs/DISCOVERED.md +250 -248
  84. package/docs/FEATURE_TEST_MATRIX.md +7 -260
  85. package/docs/GITHUB_AUTHORIZATION.md +5 -0
  86. package/docs/PROVENANCE_AND_COMPARISON.md +1 -1
  87. package/docs/RELEASE_REVIEW.md +0 -1
  88. package/docs/evidence/readme-claims.json +22 -19
  89. package/package.json +7 -4
  90. package/skills/loadout-curator/SKILL.md +105 -0
  91. package/skills/loadout-router/SKILL.md +85 -0
  92. package/MASTER_PLAN.md +0 -2207
  93. package/dist/src/core/completion.js +0 -143
  94. package/dist/src/core/handoff.js +0 -161
  95. package/docs/ACTIVE_SET.md +0 -53
  96. package/docs/COMPATIBILITY_POLICY.md +0 -22
  97. package/docs/CONVERSION_AND_SANDBOX.md +0 -27
  98. package/docs/EVALUATION_PROTOCOL_V1.md +0 -300
  99. package/docs/HEAD_TO_HEAD_EVALUATION.md +0 -79
  100. package/docs/PROVIDER_CONFIGURATION.md +0 -45
  101. package/docs/README_RESEARCH.md +0 -36
  102. package/docs/REPOSITORY_STABILIZATION.md +0 -190
  103. package/docs/SAFE_UPDATE_DEMO.md +0 -25
  104. package/docs/SCHEMA_DECISIONS.md +0 -25
  105. package/docs/SUBMISSION_COPY.md +0 -90
  106. package/docs/TEAM_POLICY.md +0 -18
  107. package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +0 -283
  108. package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +0 -116
  109. package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +0 -469
  110. package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +0 -80
  111. package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +0 -55
  112. package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +0 -228
  113. /package/dist/src/core/{agent-health-score.js → agents/agent-health-score.js} +0 -0
  114. /package/dist/src/core/{agent-versions.js → agents/agent-versions.js} +0 -0
  115. /package/dist/src/core/{conversion.js → agents/conversion.js} +0 -0
  116. /package/dist/src/core/{paths.js → agents/paths.js} +0 -0
  117. /package/dist/src/core/{runtime-tool-recipe.js → agents/runtime-tool-recipe.js} +0 -0
  118. /package/dist/src/core/{catalog-coverage.js → catalog/catalog-coverage.js} +0 -0
  119. /package/dist/src/core/{skill-security.js → catalog/skill-security.js} +0 -0
  120. /package/dist/src/core/{community.js → discovery/community.js} +0 -0
  121. /package/dist/src/core/{github-discovery.js → discovery/github-discovery.js} +0 -0
  122. /package/dist/src/core/{private-discovery.js → discovery/private-discovery.js} +0 -0
  123. /package/dist/src/core/{ranking.js → discovery/ranking.js} +0 -0
  124. /package/dist/src/core/{atomic-file.js → install/atomic-file.js} +0 -0
  125. /package/dist/src/core/{diff.js → install/diff.js} +0 -0
  126. /package/dist/src/core/{update-watch.js → install/update-watch.js} +0 -0
  127. /package/dist/src/core/{loadout-badge.js → reporting/loadout-badge.js} +0 -0
  128. /package/dist/src/core/{terminal.js → reporting/terminal.js} +0 -0
  129. /package/dist/src/core/{access.js → routing/access.js} +0 -0
  130. /package/dist/src/core/{credentials.js → routing/credentials.js} +0 -0
  131. /package/dist/src/core/{sandbox.js → runtime/sandbox.js} +0 -0
  132. /package/dist/src/core/{active-limit.js → workspace/active-limit.js} +0 -0
  133. /package/dist/src/core/{target-occupancy.js → workspace/target-occupancy.js} +0 -0
@@ -1,9 +1,11 @@
1
- import { createHash } from "node:crypto";
2
- import { cp, lstat, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
3
3
  import { join, relative, resolve, sep } from "node:path";
4
- import { loadoutHome } from "./paths.js";
4
+ import { loadoutHome } from "../agents/paths.js";
5
5
  import { analyzeUpdateSafety } from "./safety.js";
6
6
  const NAME = /^[a-z0-9][a-z0-9._-]*$/;
7
+ /** A SHA-256 digest, and nothing that could traverse a path. */
8
+ const DIGEST = /^[a-f0-9]{64}$/;
7
9
  const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
8
10
  export function parsePackageDescriptor(value) {
9
11
  if (!value || typeof value !== "object")
@@ -266,18 +268,54 @@ function registryEndpoint(base, path) {
266
268
  endpoint.hash = "";
267
269
  return endpoint;
268
270
  }
271
+ /** Registry responses are untrusted input: bound the wait and the body. */
272
+ const REGISTRY_TIMEOUT_MS = 30_000;
273
+ const REGISTRY_MAX_BYTES = 32 * 1024 * 1024;
274
+ async function registryJson(input, init = {}) {
275
+ const response = await fetch(input, {
276
+ ...init,
277
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
278
+ });
279
+ // Trust the declared length when present, but still cap what is read: a
280
+ // server may understate or omit it.
281
+ const declared = Number(response.headers.get("content-length"));
282
+ if (Number.isFinite(declared) && declared > REGISTRY_MAX_BYTES)
283
+ throw new Error("Remote registry response exceeds the size limit");
284
+ const body = await response.arrayBuffer();
285
+ if (body.byteLength > REGISTRY_MAX_BYTES)
286
+ throw new Error("Remote registry response exceeds the size limit");
287
+ const text = new TextDecoder().decode(body);
288
+ let value;
289
+ try {
290
+ value = JSON.parse(text);
291
+ }
292
+ catch {
293
+ throw new Error("Remote registry returned a malformed JSON response");
294
+ }
295
+ return { ok: response.ok, status: response.status, value };
296
+ }
269
297
  export async function fetchRemoteRegistryPackage(registry, name, version) {
270
298
  if (!NAME.test(name) || !VERSION.test(version))
271
299
  throw new Error("Invalid remote registry package name or version");
272
300
  const base = registryUrl(registry);
273
- const response = await fetch(registryEndpoint(base, `/v1/packages/${encodeURIComponent(name)}/${encodeURIComponent(version)}`));
301
+ const response = await registryJson(registryEndpoint(base, `/v1/packages/${encodeURIComponent(name)}/${encodeURIComponent(version)}`));
274
302
  if (!response.ok)
275
303
  throw new Error(`Remote registry returned ${response.status}`);
276
- const bundle = (await response.json());
304
+ const bundle = response.value;
305
+ // `digest` arrives from a remote server and is used to build a path that is
306
+ // later deleted recursively. Validate its shape before it can become one:
307
+ // a value such as "../../.." would otherwise escape the cache root.
308
+ if (typeof bundle?.digest !== "string" || !DIGEST.test(bundle.digest))
309
+ throw new Error("Remote registry returned a bundle whose digest is not a SHA-256 hex string");
277
310
  const key = createHash("sha256")
278
311
  .update(base.origin + base.pathname)
279
312
  .digest("hex");
280
- const target = join(loadoutHome(), "cache", "registry", key, name, version, bundle.digest);
313
+ const root = join(loadoutHome(), "cache", "registry", key, name, version);
314
+ const target = resolve(root, bundle.digest);
315
+ // Belt and braces: even with a validated digest, never operate on a path
316
+ // that resolved outside the directory it was meant to live in.
317
+ if (target !== root && !target.startsWith(root + sep))
318
+ throw new Error("Remote registry bundle resolved outside the cache root");
281
319
  try {
282
320
  const packed = await packPackage(target);
283
321
  if (packed.digest === bundle.digest)
@@ -286,9 +324,19 @@ export async function fetchRemoteRegistryPackage(registry, name, version) {
286
324
  catch {
287
325
  /* import below */
288
326
  }
289
- await mkdir(resolve(target, ".."), { recursive: true });
290
- await rm(target, { recursive: true, force: true });
291
- await importRegistryBundle(bundle, target);
327
+ // Import into a scratch directory and swap it in, so a failed or malicious
328
+ // import cannot leave the cache holding a half-written package.
329
+ await mkdir(root, { recursive: true });
330
+ const staging = `${target}.incoming-${randomUUID().slice(0, 8)}`;
331
+ try {
332
+ await importRegistryBundle(bundle, staging);
333
+ await rm(target, { recursive: true, force: true });
334
+ await rename(staging, target);
335
+ }
336
+ catch (error) {
337
+ await rm(staging, { recursive: true, force: true });
338
+ throw error;
339
+ }
292
340
  return { path: target, digest: bundle.digest };
293
341
  }
294
342
  export async function publishRemotePackage(root, registry, token, options = {}) {
@@ -296,7 +344,7 @@ export async function publishRemotePackage(root, registry, token, options = {})
296
344
  throw new Error("Remote registry token is required");
297
345
  const bundle = await createRegistryBundle(root);
298
346
  const base = registryUrl(registry);
299
- const response = await fetch(registryEndpoint(base, "/v1/packages"), {
347
+ const response = await registryJson(registryEndpoint(base, "/v1/packages"), {
300
348
  method: "POST",
301
349
  headers: {
302
350
  authorization: `Bearer ${token}`,
@@ -305,7 +353,7 @@ export async function publishRemotePackage(root, registry, token, options = {})
305
353
  },
306
354
  body: JSON.stringify(bundle),
307
355
  });
308
- const result = (await response.json());
356
+ const result = response.value;
309
357
  if (!response.ok)
310
358
  throw new Error(result.error ?? `Remote registry returned ${response.status}`);
311
359
  return {
@@ -38,27 +38,37 @@ const SUSPICIOUS_INSTRUCTIONS = [
38
38
  export function detectSecretKinds(content) {
39
39
  return SECRET_PATTERNS.filter((check) => check.pattern.test(content)).map((check) => check.name);
40
40
  }
41
- async function files(root) {
41
+ const MAX_INSPECTABLE_BYTES = 2_000_000;
42
+ const MAX_DEPTH = 10;
43
+ async function files(root, gaps = []) {
42
44
  const result = new Map();
43
45
  const base = resolve(root);
44
46
  try {
45
47
  const rootInfo = await stat(base);
46
- if (rootInfo.isFile() && rootInfo.size <= 2_000_000) {
48
+ if (rootInfo.isFile()) {
49
+ if (rootInfo.size > MAX_INSPECTABLE_BYTES) {
50
+ gaps.push({ path: basename(base), reason: "too-large" });
51
+ return result;
52
+ }
47
53
  result.set(basename(base), await readFile(base));
48
54
  return result;
49
55
  }
50
56
  }
51
57
  catch {
58
+ gaps.push({ path: basename(base), reason: "unreadable" });
52
59
  return result;
53
60
  }
54
61
  async function visit(directory, depth) {
55
- if (depth > 10)
62
+ if (depth > MAX_DEPTH) {
63
+ gaps.push({ path: relative(base, directory), reason: "too-deep" });
56
64
  return;
65
+ }
57
66
  let entries;
58
67
  try {
59
68
  entries = await readdir(directory, { withFileTypes: true });
60
69
  }
61
70
  catch {
71
+ gaps.push({ path: relative(base, directory), reason: "unreadable" });
62
72
  return;
63
73
  }
64
74
  for (const entry of entries) {
@@ -73,14 +83,19 @@ async function files(root) {
73
83
  }
74
84
  if (!entry.isFile())
75
85
  continue;
86
+ const shown = relative(base, absolute).split("\\").join("/");
76
87
  try {
77
88
  const info = await stat(absolute);
78
- if (info.size > 2_000_000)
89
+ if (info.size > MAX_INSPECTABLE_BYTES) {
90
+ gaps.push({ path: shown, reason: "too-large" });
79
91
  continue;
80
- result.set(relative(base, absolute).split("\\").join("/"), await readFile(absolute));
92
+ }
93
+ result.set(shown, await readFile(absolute));
81
94
  }
82
95
  catch {
83
- /* repository may change while being inspected */
96
+ // The repository may change while being inspected; either way this
97
+ // file was not reviewed, so record it rather than passing it silently.
98
+ gaps.push({ path: shown, reason: "unreadable" });
84
99
  }
85
100
  }
86
101
  }
@@ -131,8 +146,9 @@ function collectEnvironmentNames(content) {
131
146
  }
132
147
  /** Analyze changed repository files without running package code or revealing secret values. */
133
148
  export async function analyzeUpdateSafety(oldPath, newPath) {
149
+ const gaps = [];
134
150
  const oldFiles = oldPath ? await files(oldPath) : new Map();
135
- const newFiles = await files(newPath);
151
+ const newFiles = await files(newPath, gaps);
136
152
  const changed = changedPaths(oldFiles, newFiles);
137
153
  const findings = [];
138
154
  const domains = new Set();
@@ -227,6 +243,19 @@ export async function analyzeUpdateSafety(oldPath, newPath) {
227
243
  paths: [...new Set(instructionPaths)].sort(),
228
244
  names: [...instructionKinds].sort(),
229
245
  });
246
+ // Fail closed. Anything the scan could not open is exactly what a reviewer
247
+ // needs told about, so an uninspectable file requires approval rather than
248
+ // producing a clean report.
249
+ if (gaps.length) {
250
+ const reasons = [...new Set(gaps.map((gap) => gap.reason))].sort();
251
+ findings.push({
252
+ severity: "blocking",
253
+ category: "uninspectable",
254
+ message: `Safety scan could not inspect ${gaps.length} path(s) (${reasons.join(", ")}); they were not reviewed.`,
255
+ paths: [...new Set(gaps.map((gap) => gap.path))].sort(),
256
+ names: reasons,
257
+ });
258
+ }
230
259
  return {
231
260
  approvalRequired: findings.some((finding) => finding.severity === "blocking"),
232
261
  findings,
@@ -1,5 +1,5 @@
1
1
  import { basename } from "node:path";
2
- import { explainCatalogScore } from "./ranking.js";
2
+ import { explainCatalogScore } from "../discovery/ranking.js";
3
3
  const STOP_WORDS = new Set([
4
4
  "a",
5
5
  "an",
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { join, relative, resolve, sep } from "node:path";
4
- import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
5
- import { readInstallState } from "./state.js";
6
- import { listInstalledRuntimeToolSkillTargets, } from "./runtime-tools.js";
4
+ import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
5
+ import { readInstallState } from "../workspace/state.js";
6
+ import { listInstalledRuntimeToolSkillTargets, } from "../agents/runtime-tools.js";
7
7
  const MAX_SCAN_DEPTH = 6;
8
8
  function cleanFrontmatterValue(value) {
9
9
  if (!value)
@@ -1,6 +1,6 @@
1
1
  import { cp, lstat, readdir, readFile, rm } from "node:fs/promises";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
- import { ensureDirectory } from "./paths.js";
3
+ import { ensureDirectory } from "../agents/paths.js";
4
4
  import { assertSkillSecurity, scanSkillSecurity } from "./skill-security.js";
5
5
  export async function discoverSkillDirectories(root, options = {}) {
6
6
  const result = [];
@@ -4,13 +4,13 @@ import { readFile, stat } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { isDeepStrictEqual } from "node:util";
7
- import { writeFileAtomically } from "./atomic-file.js";
8
- import { loadEffectiveCatalog, validateCatalog } from "./catalog.js";
7
+ import { writeFileAtomically } from "../install/atomic-file.js";
8
+ import { loadEffectiveCatalog, validateCatalog } from "../catalog/catalog.js";
9
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";
10
+ import { inspectPackage } from "../install/package.js";
11
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
12
+ import { fetchRepositorySnapshot, normalizeRepository, } from "../install/source.js";
13
+ import { safeTerminalText } from "../reporting/terminal.js";
14
14
  const dossierDirectory = () => join(loadoutHome(), "candidates");
15
15
  const sourceVerifiedDossiers = new WeakMap();
16
16
  function installabilityFor(components) {
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { writeFileAtomically } from "./atomic-file.js";
4
- import { ensureDirectory } from "./paths.js";
3
+ import { writeFileAtomically } from "../install/atomic-file.js";
4
+ import { ensureDirectory } from "../agents/paths.js";
5
5
  export class ConnectorRequestError extends Error {
6
6
  status;
7
7
  retryAfterSeconds;
@@ -1,5 +1,5 @@
1
- import { inspectPackage } from "./package.js";
2
- import { analyzeUpdateSafety } from "./safety.js";
1
+ import { inspectPackage } from "../install/package.js";
2
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
3
3
  function statusFor(findings) {
4
4
  if (findings.some((finding) => finding.category === "secret" || finding.category === "instruction"))
5
5
  return "blocked";
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
3
- import { loadoutHome } from "./paths.js";
3
+ import { loadoutHome } from "../agents/paths.js";
4
4
  const SOURCE = "official-mcp-registry";
5
5
  const API = "https://registry.modelcontextprotocol.io/v0.1/servers";
6
6
  const ATTRIBUTION = "https://registry.modelcontextprotocol.io/docs";
@@ -1,8 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { fetchGitHubReleaseMetadata, } from "./github.js";
4
- import { writeFileAtomically } from "./atomic-file.js";
5
- import { loadoutHome } from "./paths.js";
3
+ import { fetchGitHubReleaseMetadata, } from "../runtime/github.js";
4
+ import { writeFileAtomically } from "../install/atomic-file.js";
5
+ import { loadoutHome } from "../agents/paths.js";
6
6
  const MAX_OBSERVATIONS_PER_REPOSITORY = 90;
7
7
  function observationPath(repository) {
8
8
  return join(loadoutHome(), "observations", `${repository.replace("/", "__")}.json`);
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { writeFileAtomically } from "./atomic-file.js";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
3
+ import { writeFileAtomically } from "../install/atomic-file.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
5
  const queuePath = () => join(loadoutHome(), "review-queue.json");
6
6
  function isQueue(value) {
7
7
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
3
- import { loadoutHome } from "./paths.js";
3
+ import { loadoutHome } from "../agents/paths.js";
4
4
  const SOURCE = "skills-sh";
5
5
  const API = "https://skills.sh/api/v1/skills";
6
6
  const ATTRIBUTION = "https://skills.sh/docs/api";
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, readdir } from "node:fs/promises";
3
3
  import { basename, isAbsolute, join, posix, relative, sep } from "node:path";
4
- import { enrichInventoryWithProvenance } from "./provenance.js";
5
- import { scanInstalledSkills } from "./skill-inventory.js";
6
- import { installStatePath, recordInstall } from "./state.js";
4
+ import { enrichInventoryWithProvenance } from "../catalog/provenance.js";
5
+ import { scanInstalledSkills } from "../catalog/skill-inventory.js";
6
+ import { installStatePath, recordInstall } from "../workspace/state.js";
7
7
  import { runMutationTransaction } from "./transaction.js";
8
8
  const issuedAdoptionPlans = new WeakMap();
9
9
  function safeRelativePath(root, path) {
@@ -1,13 +1,13 @@
1
- import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
2
- import { loadEffectiveCatalog } from "./catalog.js";
3
- import { detectAgents } from "./paths.js";
4
- import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "./profiles.js";
1
+ import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
2
+ import { loadEffectiveCatalog, } from "../catalog/catalog.js";
3
+ import { detectAgents } from "../agents/paths.js";
4
+ import { isStableSkillSelected, isPowerSkillSelected, resolveCatalogProfile, } from "../catalog/profiles.js";
5
5
  import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, installedAgents, planManagedProfileReconciliation, } from "./install.js";
6
6
  import { fetchRepositorySnapshot, } from "./source.js";
7
- import { analyzeInstallPlanSafety, } from "./safety.js";
8
- import { formatModelApiAccess } from "./access.js";
9
- import { recordInstalledProfile } from "./profile-state.js";
10
- import { readInstallState } from "./state.js";
7
+ import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
8
+ import { formatModelApiAccess, } from "../routing/access.js";
9
+ import { recordInstalledProfile } from "../workspace/profile-state.js";
10
+ import { readInstallState } from "../workspace/state.js";
11
11
  async function parallelMap(values, concurrency, worker) {
12
12
  const results = new Array(values.length);
13
13
  let cursor = 0;
@@ -240,11 +240,11 @@ export function formatPreparedCatalogInstall(prepared, options = {}) {
240
240
  const quarantined = prepared.skipped.filter((item) => item.kind === "quarantined");
241
241
  const retired = prepared.reconciliation?.obsoleteUnits ?? [];
242
242
  const modeLabel = prepared.selection.mode === "maximum"
243
- ? "Maximum Library"
243
+ ? "Maximum"
244
244
  : prepared.selection.mode === "power"
245
- ? "Power Boost"
245
+ ? "Power"
246
246
  : prepared.selection.mode === "stable"
247
- ? "Stable Boost"
247
+ ? "Stable"
248
248
  : "Custom";
249
249
  const lines = [
250
250
  `Loadout: ${modeLabel}`,
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, rename, rm, stat, utimes, } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { ensureDirectory } from "./paths.js";
4
+ import { ensureDirectory } from "../agents/paths.js";
5
5
  const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6
6
  function parseOwner(content) {
7
7
  try {
@@ -1,12 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { cp, lstat, rm } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, posix, relative, win32, } from "node:path";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
5
- import { planAdapterSkillInstall } from "./adapters.js";
6
- import { applySkillPlan, detectInstallConflicts } from "./skills.js";
7
- import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "./state.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
+ import { planAdapterSkillInstall } from "../agents/adapters.js";
6
+ import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
7
+ import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "../workspace/state.js";
8
8
  import { runMutationTransaction } from "./transaction.js";
9
- import { inspectTargetOccupancy } from "./target-occupancy.js";
9
+ import { inspectTargetOccupancy } from "../workspace/target-occupancy.js";
10
10
  export function installedAgents(agents, requested) {
11
11
  const available = agents.filter((agent) => agent.installed);
12
12
  if (!requested || requested.length === 0)
@@ -1,8 +1,8 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
- import { discoverSkillDirectories } from "./skills.js";
4
- import { discoverMcpManifests } from "./mcp.js";
5
- import { discoverResources } from "./components.js";
3
+ import { discoverSkillDirectories } from "../catalog/skills.js";
4
+ import { discoverMcpManifests } from "../runtime/mcp.js";
5
+ import { discoverResources } from "../catalog/components.js";
6
6
  const portableRelative = (root, path) => relative(root, path).split(/[\\/]/).join("/") || ".";
7
7
  function frontmatter(path) {
8
8
  return readFile(join(path, "SKILL.md"), "utf8").then((content) => {
@@ -1,13 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { cp, lstat, readFile, readdir, rm } from "node:fs/promises";
3
3
  import { join, relative, sep } from "node:path";
4
- import { analyzeUpdateSafety } from "./safety.js";
5
- import { enrichInventoryWithProvenance, } from "./provenance.js";
6
- import { scanInstalledSkills } from "./skill-inventory.js";
7
- import { installStatePath, recordInstallBatch } from "./state.js";
4
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
5
+ import { enrichInventoryWithProvenance, } from "../catalog/provenance.js";
6
+ import { scanInstalledSkills } from "../catalog/skill-inventory.js";
7
+ import { installStatePath, recordInstallBatch } from "../workspace/state.js";
8
8
  import { runMutationTransaction } from "./transaction.js";
9
9
  import { repositoryCachePath } from "./source.js";
10
- import { validateSkillDirectory } from "./skills.js";
10
+ import { validateSkillDirectory } from "../catalog/skills.js";
11
11
  const issuedPlans = new WeakMap();
12
12
  const IGNORED_TREE_NAMES = new Set([".git", ".cache", "node_modules"]);
13
13
  function slug(value) {
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, rm } from "node:fs/promises";
3
- import { forgetInstall, installStatePath, readInstallState } from "./state.js";
4
- import { writeMcpConfigPlan } from "./mcp.js";
3
+ import { forgetInstall, installStatePath, readInstallState, } from "../workspace/state.js";
4
+ import { writeMcpConfigPlan } from "../runtime/mcp.js";
5
5
  import { runMutationTransaction } from "./transaction.js";
6
- import { managedFileReadPath } from "./active-set.js";
7
- import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "./codex-mcp.js";
6
+ import { managedFileReadPath } from "../workspace/active-set.js";
7
+ import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "../agents/codex-mcp.js";
8
8
  export async function planRemove(packageId) {
9
9
  const state = await readInstallState();
10
10
  const record = state.installs.find((entry) => entry.packageId === packageId);
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { readFile, writeFile, mkdir, readdir, rename, rm, lstat, } from "node:fs/promises";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
- import { loadoutHome, ensureDirectory, userHome } from "./paths.js";
4
+ import { loadoutHome, ensureDirectory, userHome } from "../agents/paths.js";
5
5
  export async function createSnapshot(paths, options = {}) {
6
6
  const normalizedRoots = [...new Set(paths.map((path) => resolve(path)))];
7
7
  const snapshot = {
@@ -4,11 +4,28 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { execFile } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
- import { loadoutHome, ensureDirectory } from "./paths.js";
7
+ import { loadoutHome, ensureDirectory } from "../agents/paths.js";
8
8
  const execFileAsync = promisify(execFile);
9
+ /**
10
+ * Bounds apply to every fetch, not only to callers who remember to ask. An
11
+ * unbounded clone of an untrusted repository can hang a Git process forever or
12
+ * fill the disk, so callers may raise a limit but none may omit one.
13
+ */
14
+ export const REPOSITORY_FETCH_DEFAULTS = {
15
+ /** A reviewed skill repository clones well inside this. */
16
+ timeoutMs: 120_000,
17
+ maxBytes: 256 * 1024 * 1024,
18
+ maxFiles: 20_000,
19
+ };
20
+ function withFetchDefaults(options) {
21
+ return {
22
+ ...options,
23
+ timeoutMs: options.timeoutMs ?? REPOSITORY_FETCH_DEFAULTS.timeoutMs,
24
+ maxBytes: options.maxBytes ?? REPOSITORY_FETCH_DEFAULTS.maxBytes,
25
+ maxFiles: options.maxFiles ?? REPOSITORY_FETCH_DEFAULTS.maxFiles,
26
+ };
27
+ }
9
28
  async function enforceRepositoryBounds(root, options) {
10
- if (options.maxBytes === undefined && options.maxFiles === undefined)
11
- return;
12
29
  let bytes = 0;
13
30
  let files = 0;
14
31
  const pending = [root];
@@ -35,8 +52,6 @@ async function enforceRepositoryBounds(root, options) {
35
52
  }
36
53
  }
37
54
  export function validateGitHubTreeBounds(value, options) {
38
- if (options.maxBytes === undefined && options.maxFiles === undefined)
39
- return;
40
55
  if (!value || typeof value !== "object" || Array.isArray(value))
41
56
  throw new Error("GitHub tree preflight returned an invalid response");
42
57
  const response = value;
@@ -63,8 +78,6 @@ export function validateGitHubTreeBounds(value, options) {
63
78
  }
64
79
  }
65
80
  async function enforceGitHubTreeBounds(repository, commit, options) {
66
- if (options.maxBytes === undefined && options.maxFiles === undefined)
67
- return;
68
81
  const signal = AbortSignal.timeout(options.timeoutMs ?? 120_000);
69
82
  const token = process.env.GITHUB_TOKEN?.trim();
70
83
  const response = await (options.fetcher ?? fetch)(`https://api.github.com/repos/${repository}/git/trees/${commit}?recursive=1`, {
@@ -181,7 +194,8 @@ function normalizeRef(ref) {
181
194
  throw new Error(`Invalid Git ref: ${ref}`);
182
195
  return ref;
183
196
  }
184
- export async function fetchRepositorySnapshot(input, options = {}) {
197
+ export async function fetchRepositorySnapshot(input, requested = {}) {
198
+ const options = withFetchDefaults(requested);
185
199
  const repository = normalizeRepository(input);
186
200
  const gitEnvironment = await isolatedGitEnvironment(loadoutHome());
187
201
  const ref = options.ref ? normalizeRef(options.ref) : undefined;
@@ -1,17 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, resolve, sep } from "node:path";
4
- import { loadEffectiveCatalog } from "./catalog.js";
4
+ import { loadEffectiveCatalog } from "../catalog/catalog.js";
5
5
  import { installedAgents } from "./install.js";
6
- import { orderManifestPackages, readManifest, writeLockfile, } from "./manifest.js";
7
- import { detectAgents, userHome } from "./paths.js";
6
+ import { orderManifestPackages, readManifest, writeLockfile, } from "../workspace/manifest.js";
7
+ import { detectAgents, userHome } from "../agents/paths.js";
8
8
  import { fetchGitSnapshot, fetchRepositorySnapshot } from "./source.js";
9
- import { addRootFileExports, buildUniversalPackagePlan } from "./components.js";
10
- import { analyzeInstallPlanSafety, } from "./safety.js";
11
- import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "./registry.js";
12
- import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "./mcp.js";
13
- import { applySkillPlan, detectInstallConflicts } from "./skills.js";
14
- import { installStatePath, readInstallState, recordInstallTransaction, } from "./state.js";
9
+ import { addRootFileExports, buildUniversalPackagePlan, } from "../catalog/components.js";
10
+ import { analyzeInstallPlanSafety, } from "../catalog/safety.js";
11
+ import { fetchRemoteRegistryPackage, packPackage, resolveRegistryPackage, } from "../catalog/registry.js";
12
+ import { discoverMcpManifests, planMcpConfigBatch, writeMcpConfigPlan, } from "../runtime/mcp.js";
13
+ import { applySkillPlan, detectInstallConflicts } from "../catalog/skills.js";
14
+ import { installStatePath, readInstallState, recordInstallTransaction, } from "../workspace/state.js";
15
15
  import { runMutationTransaction } from "./transaction.js";
16
16
  function registrySourceKey(source) {
17
17
  if (source.type === "registry")
@@ -1,7 +1,7 @@
1
1
  import { lstat, mkdir, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
- import { ensureDirectory, loadoutHome } from "./paths.js";
4
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
5
5
  import { createSnapshot, readSnapshot, recordSnapshotPostMutationState, restoreSnapshot, } from "./snapshot.js";
6
6
  import { acquireFileLock, withFileLock, } from "./file-lock.js";
7
7
  export const transactionRoot = () => join(loadoutHome(), "staging");
@@ -2,11 +2,11 @@ import { execFile } from "node:child_process";
2
2
  import { readdir, rm, rmdir } from "node:fs/promises";
3
3
  import { dirname, parse, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { loadoutHome } from "./paths.js";
5
+ import { loadoutHome } from "../agents/paths.js";
6
6
  import { applyRemove, planRemove } from "./remove.js";
7
- import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "./runtime-tools.js";
8
- import { applyNativeSchedulerBundle, planNativeScheduler, } from "./scheduler.js";
9
- import { readInstallState } from "./state.js";
7
+ import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "../agents/runtime-tools.js";
8
+ import { applyNativeSchedulerBundle, planNativeScheduler, } from "../runtime/scheduler.js";
9
+ import { readInstallState } from "../workspace/state.js";
10
10
  export async function buildUninstallPlan(dependencies = {}) {
11
11
  const stateHome = loadoutHome();
12
12
  const state = await readInstallState();
@@ -3,11 +3,11 @@ import { basename, join } from "node:path";
3
3
  import { fetchRepositorySnapshot, resolveRepositoryHead } from "./source.js";
4
4
  import { repositoryCachePath } from "./source.js";
5
5
  import { diffRepositorySnapshots } from "./diff.js";
6
- import { hashDirectory, readInstallState } from "./state.js";
7
- import { analyzeUpdateSafety } from "./safety.js";
8
- import { detectAgents, loadoutHome } from "./paths.js";
6
+ import { hashDirectory, readInstallState } from "../workspace/state.js";
7
+ import { analyzeUpdateSafety } from "../catalog/safety.js";
8
+ import { detectAgents, loadoutHome } from "../agents/paths.js";
9
9
  import { applySkillInstall, buildSkillPlan, installedAgents, } from "./install.js";
10
- import { discoverSkillDirectories, validateSkillDirectory, } from "./skills.js";
10
+ import { discoverSkillDirectories, validateSkillDirectory, } from "../catalog/skills.js";
11
11
  function managedUnitIds(state, packageId) {
12
12
  return [
13
13
  ...new Set((state.activations ?? [])