loadout-ai 0.6.0 → 0.7.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 (103) hide show
  1. package/README.md +122 -19
  2. package/catalog/discovered.json +25557 -24308
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/commands/agents.js +6 -23
  5. package/dist/src/commands/catalog.js +130 -33
  6. package/dist/src/commands/health.js +6 -6
  7. package/dist/src/commands/inventory.js +19 -120
  8. package/dist/src/commands/lifecycle.js +13 -51
  9. package/dist/src/commands/mcp.js +10 -73
  10. package/dist/src/commands/setup.js +5 -5
  11. package/dist/src/commands/sharing.js +11 -108
  12. package/dist/src/commands/support.js +20 -9
  13. package/dist/src/core/{adapters.js → agents/adapters.js} +2 -2
  14. package/dist/src/core/{agent-inspection.js → agents/agent-inspection.js} +26 -6
  15. package/dist/src/core/{codex-mcp.js → agents/codex-mcp.js} +1 -1
  16. package/dist/src/core/{health-score-evidence.js → agents/health-score-evidence.js} +8 -8
  17. package/dist/src/core/{runtime-tools.js → agents/runtime-tools.js} +2 -2
  18. package/dist/src/core/{audit.js → catalog/audit.js} +4 -4
  19. package/dist/src/core/{catalog.js → catalog/catalog.js} +5 -5
  20. package/dist/src/core/{components.js → catalog/components.js} +2 -2
  21. package/dist/src/core/{conformance.js → catalog/conformance.js} +2 -2
  22. package/dist/src/core/{profiles.js → catalog/profiles.js} +2 -2
  23. package/dist/src/core/{provenance.js → catalog/provenance.js} +3 -3
  24. package/dist/src/core/{registry.js → catalog/registry.js} +1 -1
  25. package/dist/src/core/{skill-compare.js → catalog/skill-compare.js} +1 -1
  26. package/dist/src/core/{skill-inventory.js → catalog/skill-inventory.js} +3 -3
  27. package/dist/src/core/{skills.js → catalog/skills.js} +1 -1
  28. package/dist/src/core/{candidate-intelligence.js → discovery/candidate-intelligence.js} +6 -6
  29. package/dist/src/core/{discovery-connector.js → discovery/discovery-connector.js} +2 -2
  30. package/dist/src/core/{evaluate.js → discovery/evaluate.js} +2 -2
  31. package/dist/src/core/{mcp-registry-discovery.js → discovery/mcp-registry-discovery.js} +1 -1
  32. package/dist/src/core/{observations.js → discovery/observations.js} +3 -3
  33. package/dist/src/core/{review-queue.js → discovery/review-queue.js} +2 -2
  34. package/dist/src/core/{skills-sh-discovery.js → discovery/skills-sh-discovery.js} +1 -1
  35. package/dist/src/core/{adopt.js → install/adopt.js} +3 -3
  36. package/dist/src/core/{catalog-install.js → install/catalog-install.js} +11 -11
  37. package/dist/src/core/{file-lock.js → install/file-lock.js} +1 -1
  38. package/dist/src/core/{install.js → install/install.js} +5 -5
  39. package/dist/src/core/{package.js → install/package.js} +3 -3
  40. package/dist/src/core/{reconcile.js → install/reconcile.js} +5 -5
  41. package/dist/src/core/{remove.js → install/remove.js} +4 -4
  42. package/dist/src/core/{snapshot.js → install/snapshot.js} +1 -1
  43. package/dist/src/core/{source.js → install/source.js} +1 -1
  44. package/dist/src/core/{sync.js → install/sync.js} +9 -9
  45. package/dist/src/core/{transaction.js → install/transaction.js} +1 -1
  46. package/dist/src/core/{uninstall.js → install/uninstall.js} +4 -4
  47. package/dist/src/core/{update.js → install/update.js} +4 -4
  48. package/dist/src/core/{cli-guide.js → reporting/cli-guide.js} +29 -45
  49. package/dist/src/core/{doctor.js → reporting/doctor.js} +3 -6
  50. package/dist/src/core/{freshness-alerts.js → reporting/freshness-alerts.js} +5 -5
  51. package/dist/src/core/{health.js → reporting/health.js} +6 -6
  52. package/dist/src/core/{loadout-card.js → reporting/loadout-card.js} +1 -1
  53. package/dist/src/core/{readme-claims.js → reporting/readme-claims.js} +1 -1
  54. package/dist/src/core/{readme-facts.js → reporting/readme-facts.js} +2 -2
  55. package/dist/src/core/{share-report.js → reporting/share-report.js} +3 -3
  56. package/dist/src/core/{upgrade.js → reporting/upgrade.js} +7 -7
  57. package/dist/src/core/routing/first-party-skills.js +118 -0
  58. package/dist/src/core/{handoff.js → routing/handoff.js} +120 -3
  59. package/dist/src/core/{model-config.js → routing/model-config.js} +4 -4
  60. package/dist/src/core/{route.js → routing/route.js} +325 -34
  61. package/dist/src/core/{api.js → runtime/api.js} +4 -4
  62. package/dist/src/core/{canary.js → runtime/canary.js} +1 -1
  63. package/dist/src/core/{github.js → runtime/github.js} +1 -1
  64. package/dist/src/core/{mcp-recipes.js → runtime/mcp-recipes.js} +1 -1
  65. package/dist/src/core/{mcp.js → runtime/mcp.js} +2 -2
  66. package/dist/src/core/{scheduler.js → runtime/scheduler.js} +4 -4
  67. package/dist/src/core/{active-policy.js → workspace/active-policy.js} +2 -2
  68. package/dist/src/core/{active-set.js → workspace/active-set.js} +2 -2
  69. package/dist/src/core/{improve.js → workspace/improve.js} +3 -3
  70. package/dist/src/core/{manifest.js → workspace/manifest.js} +2 -2
  71. package/dist/src/core/{outcomes.js → workspace/outcomes.js} +3 -3
  72. package/dist/src/core/{portable.js → workspace/portable.js} +3 -3
  73. package/dist/src/core/{profile-state.js → workspace/profile-state.js} +1 -1
  74. package/dist/src/core/{recommend.js → workspace/recommend.js} +1 -1
  75. package/dist/src/core/{state.js → workspace/state.js} +3 -3
  76. package/docs/DISCOVERED.md +247 -246
  77. package/docs/evidence/readme-claims.json +22 -19
  78. package/package.json +2 -1
  79. package/skills/loadout-curator/SKILL.md +105 -0
  80. package/skills/loadout-router/SKILL.md +120 -0
  81. /package/dist/src/core/{agent-health-score.js → agents/agent-health-score.js} +0 -0
  82. /package/dist/src/core/{agent-versions.js → agents/agent-versions.js} +0 -0
  83. /package/dist/src/core/{conversion.js → agents/conversion.js} +0 -0
  84. /package/dist/src/core/{paths.js → agents/paths.js} +0 -0
  85. /package/dist/src/core/{runtime-tool-recipe.js → agents/runtime-tool-recipe.js} +0 -0
  86. /package/dist/src/core/{catalog-coverage.js → catalog/catalog-coverage.js} +0 -0
  87. /package/dist/src/core/{safety.js → catalog/safety.js} +0 -0
  88. /package/dist/src/core/{skill-security.js → catalog/skill-security.js} +0 -0
  89. /package/dist/src/core/{community.js → discovery/community.js} +0 -0
  90. /package/dist/src/core/{github-discovery.js → discovery/github-discovery.js} +0 -0
  91. /package/dist/src/core/{private-discovery.js → discovery/private-discovery.js} +0 -0
  92. /package/dist/src/core/{ranking.js → discovery/ranking.js} +0 -0
  93. /package/dist/src/core/{atomic-file.js → install/atomic-file.js} +0 -0
  94. /package/dist/src/core/{diff.js → install/diff.js} +0 -0
  95. /package/dist/src/core/{update-watch.js → install/update-watch.js} +0 -0
  96. /package/dist/src/core/{completion.js → reporting/completion.js} +0 -0
  97. /package/dist/src/core/{loadout-badge.js → reporting/loadout-badge.js} +0 -0
  98. /package/dist/src/core/{terminal.js → reporting/terminal.js} +0 -0
  99. /package/dist/src/core/{access.js → routing/access.js} +0 -0
  100. /package/dist/src/core/{credentials.js → routing/credentials.js} +0 -0
  101. /package/dist/src/core/{sandbox.js → runtime/sandbox.js} +0 -0
  102. /package/dist/src/core/{active-limit.js → workspace/active-limit.js} +0 -0
  103. /package/dist/src/core/{target-occupancy.js → workspace/target-occupancy.js} +0 -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,7 +4,7 @@ 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
9
  async function enforceRepositoryBounds(root, options) {
10
10
  if (options.maxBytes === undefined && options.maxFiles === 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 ?? [])
@@ -1,42 +1,41 @@
1
1
  /** Plain-language entry points for people using the CLI, not maintaining it. */
2
2
  export const BEGINNER_GUIDE = `
3
- START HERE
3
+ Loadout — manage the skills, tools, and MCP servers your AI agents use.
4
4
 
5
- 1. See what Loadout currently manages
6
- loadout library
7
- loadout scan
8
- loadout reconcile
5
+ FIRST TIME
9
6
 
10
- 2. Preview the recommended everyday setup
11
- loadout setup --mode stable
12
- loadout profiles
7
+ loadout doctor which agents you have, and whether they are healthy
8
+ loadout setup --mode stable preview 30 reviewed skills (nothing changes)
9
+ loadout setup --mode stable --yes install them
13
10
 
14
- 3. Find additions that fit the project in this folder
15
- loadout recommend --project .
16
- loadout optimize --project .
11
+ IN A PROJECT
17
12
 
18
- 4. Check for safer updates and new discoveries
19
- loadout health
20
- loadout alerts
21
- loadout candidate list --limit 10
13
+ loadout recommend --project . what this codebase actually needs
14
+ loadout optimize --project . propose an active set for it
15
+ loadout status health grade and what is managed
22
16
 
23
- 5. See reviewed MCP integrations and their credential needs
24
- loadout mcp-recipe
25
- loadout mcp-recipe --credential-free
26
- loadout mcp-recipe playwright --agent claude-code
17
+ WHILE YOU WORK
27
18
 
28
- If you decide to install something, Loadout shows a preview first and creates a
29
- snapshot before changing managed files. Recover with: loadout rollback
19
+ loadout route <describe a task> which model tier fits, and what it costs
20
+ loadout route --conserve <task> cheaper tiers when you are low on quota
21
+ loadout handoff send codex "..." give a task to your other agent
30
22
 
31
- Reconcile is also read-only unless you add --yes. Exact matches can be managed
32
- without rewriting them; outdated replacements remain a separate explicit choice.
23
+ USE IT INSIDE YOUR AGENT
33
24
 
34
- Setup reconciles the complete selected profile. To add one catalog package without
35
- retiring the current managed profile, use:
36
- loadout install --mode custom --package <id>
25
+ loadout skills install loadout-router --yes
37
26
 
38
- Nothing above changes your agents. For the full maintainer/tooling surface, run:
39
- loadout advanced
27
+ Then just ask your agent "which model should I use for this?" instead of
28
+ coming back here.
29
+
30
+ KEEPING CURRENT
31
+
32
+ loadout health safer updates and local drift
33
+ loadout alerts archived, stale, or changed sources
34
+
35
+ Every command that writes previews first and snapshots before it changes
36
+ anything. Undo with: loadout rollback
37
+
38
+ More commands: loadout advanced
40
39
  `.trim();
41
40
  export const ADVANCED_GUIDE = [
42
41
  "ADVANCED COMMANDS",
@@ -44,8 +43,8 @@ export const ADVANCED_GUIDE = [
44
43
  "Hidden from the first screen so daily use stays focused.",
45
44
  "",
46
45
  "Discovery: candidate, discover, review-queue, evaluate, skill-audit",
47
- "Sharing: init, lock, export, import, share, card, badge",
48
- "Integrations: mcp-recipe, mcp-config, credentials, models, handoff, convert",
46
+ "Sharing: init, lock, export, import, share, card",
47
+ "Integrations: mcp-recipe, mcp-config, credentials, models, handoff, skills, convert",
49
48
  "Lifecycle: plan, adopt, uninstall, profiles, autopilot, tool, watch",
50
49
  "",
51
50
  "`loadout <command> --help` for options. Mutations preview first or require --yes.",
@@ -64,29 +63,18 @@ export const HIDDEN_FROM_FIRST_SCREEN = new Set([
64
63
  "import",
65
64
  "audit",
66
65
  "create",
67
- "pack",
68
- "publish",
69
66
  "add",
70
67
  "unadd",
71
68
  "share",
72
69
  "card",
73
- "badge",
74
- "compare-loadouts",
75
70
  "search",
76
71
  // analytics / evidence / discovery
77
72
  "report",
78
- "outcomes",
79
- "outcome",
80
- "improve",
81
- "improve-feedback",
82
- "compare",
83
73
  "versions",
84
74
  "skill-audit",
85
- "capabilities",
86
75
  "candidate",
87
76
  "discover",
88
77
  "review-queue",
89
- "review",
90
78
  "inspect",
91
79
  "propose",
92
80
  "evaluate",
@@ -97,11 +85,7 @@ export const HIDDEN_FROM_FIRST_SCREEN = new Set([
97
85
  "completion",
98
86
  "mcp-recipe",
99
87
  "mcp-config",
100
- "codex-mcp-config",
101
- "sandbox-run",
102
88
  "convert",
103
- "canary",
104
- "serve",
105
89
  // lifecycle extras
106
90
  "plan",
107
91
  "adopt",
@@ -1,7 +1,7 @@
1
1
  import { access, constants } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { detectAgents, directoryExists, loadoutHome, userHome, } from "./paths.js";
4
- import { inspectAgents } from "./agent-inspection.js";
3
+ import { detectAgents, directoryExists, loadoutHome, userHome, } from "../agents/paths.js";
4
+ import { inspectAgents } from "../agents/agent-inspection.js";
5
5
  async function writable(path) {
6
6
  // A not-yet-created directory is healthy when any existing ancestor is
7
7
  // writable; this handles first-run paths such as ~/.agents/skills.
@@ -65,10 +65,7 @@ export async function runDoctor() {
65
65
  export function formatDoctorReport(report, options = {}) {
66
66
  const installed = report.agents.filter((a) => a.agent.installed);
67
67
  const notInstalled = report.agents.filter((a) => !a.agent.installed);
68
- const allIssues = [
69
- ...report.issues,
70
- ...installed.flatMap((a) => a.issues),
71
- ];
68
+ const allIssues = [...report.issues, ...installed.flatMap((a) => a.issues)];
72
69
  const grade = allIssues.length === 0 && installed.length > 0
73
70
  ? "HEALTHY"
74
71
  : allIssues.length <= 2
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
- import { writeFileAtomically } from "./atomic-file.js";
5
- import { loadEffectiveCatalog } from "./catalog.js";
6
- import { ensureDirectory, loadoutHome } from "./paths.js";
7
- import { readInstallState } from "./state.js";
8
- import { buildUpdatePlan } from "./update.js";
4
+ import { writeFileAtomically } from "../install/atomic-file.js";
5
+ import { loadEffectiveCatalog } from "../catalog/catalog.js";
6
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
7
+ import { readInstallState } from "../workspace/state.js";
8
+ import { buildUpdatePlan } from "../install/update.js";
9
9
  const decisionsPath = () => join(loadoutHome(), "alert-decisions.json");
10
10
  function alertId(packageId, kind, evidence) {
11
11
  return `${packageId}-${kind}-${createHash("sha256")
@@ -1,12 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { managedFileReadPath } from "./active-set.js";
5
- import { detectAgents } from "./paths.js";
6
- import { readInstallState } from "./state.js";
7
- import { buildUpdatePlan } from "./update.js";
8
- import { codexMcpServerFingerprint } from "./codex-mcp.js";
9
- import { listInstalledRuntimeToolSkillTargets, listInstalledRuntimeTools, } from "./runtime-tools.js";
4
+ import { managedFileReadPath } from "../workspace/active-set.js";
5
+ import { detectAgents } from "../agents/paths.js";
6
+ import { readInstallState } from "../workspace/state.js";
7
+ import { buildUpdatePlan } from "../install/update.js";
8
+ import { codexMcpServerFingerprint } from "../agents/codex-mcp.js";
9
+ import { listInstalledRuntimeToolSkillTargets, listInstalledRuntimeTools, } from "../agents/runtime-tools.js";
10
10
  async function drift(record, activations) {
11
11
  const changed = [];
12
12
  for (const file of record.files) {
@@ -1,4 +1,4 @@
1
- import { buildLocalAgentHealthScores } from "./health-score-evidence.js";
1
+ import { buildLocalAgentHealthScores } from "../agents/health-score-evidence.js";
2
2
  import { buildHealthReport, gradeHealth } from "./health.js";
3
3
  import { buildPrivacySafeReport, } from "./share-report.js";
4
4
  export async function buildLoadoutCard(options = {}) {
@@ -1,4 +1,4 @@
1
- import { formatSchemaError, readmeClaimManifestSchema, } from "../shared/schemas.js";
1
+ import { formatSchemaError, readmeClaimManifestSchema, } from "../../shared/schemas.js";
2
2
  export { readmeClaimManifestSchema };
3
3
  /** Parse the versioned index of evidence for material README statements. */
4
4
  export function parseReadmeClaimManifest(value) {
@@ -1,5 +1,5 @@
1
- import { supportedAdapterNames } from "./adapters.js";
2
- import { buildCatalogCoverage } from "./catalog-coverage.js";
1
+ import { supportedAdapterNames, } from "../agents/adapters.js";
2
+ import { buildCatalogCoverage } from "../catalog/catalog-coverage.js";
3
3
  function profileFacts(allowlist) {
4
4
  return {
5
5
  sources: Object.keys(allowlist).length,
@@ -1,7 +1,7 @@
1
1
  import { dirname } from "node:path";
2
- import { writeFileAtomically } from "./atomic-file.js";
3
- import { ensureDirectory } from "./paths.js";
4
- import { readInstallState } from "./state.js";
2
+ import { writeFileAtomically } from "../install/atomic-file.js";
3
+ import { ensureDirectory } from "../agents/paths.js";
4
+ import { readInstallState } from "../workspace/state.js";
5
5
  export function parsePrivacySafeLoadoutReport(value) {
6
6
  if (!value || typeof value !== "object" || Array.isArray(value))
7
7
  throw new Error("Privacy-safe Loadout report must be an object");
@@ -1,11 +1,11 @@
1
- import { buildAdapterCapabilityGaps, } from "./adapters.js";
2
- import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./catalog-install.js";
3
- import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
4
- import { loadEffectiveCatalog } from "./catalog.js";
1
+ import { buildAdapterCapabilityGaps, } from "../agents/adapters.js";
2
+ import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "../install/catalog-install.js";
3
+ import { DEFAULT_ACTIVE_SKILL_LIMIT } from "../workspace/active-limit.js";
4
+ import { loadEffectiveCatalog, } from "../catalog/catalog.js";
5
5
  import { buildHealthReport, formatHealthReport } from "./health.js";
6
- import { buildLocalAgentHealthScores } from "./health-score-evidence.js";
7
- import { formatRecommendations, personalizeRecommendations, recommendPackages, scanProject, } from "./recommend.js";
8
- import { readLocalOutcomes } from "./outcomes.js";
6
+ import { buildLocalAgentHealthScores } from "../agents/health-score-evidence.js";
7
+ import { formatRecommendations, personalizeRecommendations, recommendPackages, scanProject, } from "../workspace/recommend.js";
8
+ import { readLocalOutcomes, } from "../workspace/outcomes.js";
9
9
  /**
10
10
  * Builds the complete first-run preview without changing agent or Loadout state.
11
11
  * Network access is limited to the same immutable catalog preparation performed by
@@ -0,0 +1,118 @@
1
+ import { cp, rm, stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { detectAgents } from "../agents/paths.js";
5
+ export const FIRST_PARTY_SKILLS = [
6
+ {
7
+ id: "loadout-router",
8
+ displayName: "Loadout Router",
9
+ summary: "Lets an agent pick the right model tier for a task and hand work to another agent",
10
+ },
11
+ {
12
+ id: "loadout-curator",
13
+ displayName: "Loadout Curator",
14
+ summary: "Lets an agent decide which skills and MCP servers should be active for the current repository",
15
+ },
16
+ ];
17
+ /**
18
+ * Locate the bundled `skills/` directory. Resolution walks upward so it works
19
+ * both from `src/` under tsx and from `dist/src/` in the published package.
20
+ */
21
+ export async function bundledSkillsRoot() {
22
+ let dir = dirname(fileURLToPath(import.meta.url));
23
+ for (let depth = 0; depth < 5; depth += 1) {
24
+ const candidate = join(dir, "skills");
25
+ try {
26
+ if ((await stat(candidate)).isDirectory())
27
+ return candidate;
28
+ }
29
+ catch {
30
+ // Keep walking toward the package root.
31
+ }
32
+ dir = dirname(dir);
33
+ }
34
+ throw new Error("Bundled skills directory not found; this build may be incomplete.");
35
+ }
36
+ export function findFirstPartySkill(id) {
37
+ const skill = FIRST_PARTY_SKILLS.find((item) => item.id === id);
38
+ if (!skill)
39
+ throw new Error(`Unknown Loadout skill '${id}'. Available: ${FIRST_PARTY_SKILLS.map((s) => s.id).join(", ")}`);
40
+ return skill;
41
+ }
42
+ async function directoryExists(path) {
43
+ try {
44
+ return (await stat(path)).isDirectory();
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ /**
51
+ * Build a preview of where one first-party skill would be installed. Nothing is
52
+ * written; the caller decides whether to apply.
53
+ */
54
+ export async function planFirstPartySkill(id, options = {}) {
55
+ const skill = findFirstPartySkill(id);
56
+ const source = join(await bundledSkillsRoot(), skill.id);
57
+ if (!(await directoryExists(source)))
58
+ throw new Error(`Bundled skill '${skill.id}' is missing from this build.`);
59
+ const detected = options.detected ?? (await detectAgents());
60
+ const installed = detected.filter((agent) => agent.installed);
61
+ const requested = options.agents?.length
62
+ ? installed.filter((agent) => options.agents.includes(agent.id))
63
+ : installed;
64
+ const targets = [];
65
+ for (const agent of requested) {
66
+ const destination = join(agent.skillsDirectory, skill.id);
67
+ targets.push({
68
+ agent: agent.id,
69
+ displayName: agent.displayName,
70
+ destination,
71
+ replacing: await directoryExists(destination),
72
+ });
73
+ }
74
+ return { skill, source, targets };
75
+ }
76
+ export async function applyFirstPartySkill(plan) {
77
+ for (const target of plan.targets) {
78
+ // Replace wholesale so a reinstall cannot leave files from an older
79
+ // version of the skill behind.
80
+ if (target.replacing)
81
+ await rm(target.destination, { recursive: true });
82
+ await cp(plan.source, target.destination, { recursive: true });
83
+ }
84
+ }
85
+ export async function removeFirstPartySkill(plan) {
86
+ for (const target of plan.targets)
87
+ if (target.replacing)
88
+ await rm(target.destination, { recursive: true });
89
+ }
90
+ export function formatFirstPartySkillList(installedIds = new Set()) {
91
+ const lines = ["Skills that ship with Loadout:", ""];
92
+ for (const skill of FIRST_PARTY_SKILLS) {
93
+ const marker = installedIds.has(skill.id) ? "✓" : "○";
94
+ lines.push(` ${marker} ${skill.id} — ${skill.summary}`);
95
+ }
96
+ lines.push("", "Install with: loadout skills install <id> --yes", "These teach your agents to use Loadout from inside a conversation.");
97
+ return lines.join("\n");
98
+ }
99
+ export function formatFirstPartySkillPlan(plan) {
100
+ if (!plan.targets.length)
101
+ return `No installed agents detected, so '${plan.skill.id}' has nowhere to go. Install an agent first.`;
102
+ const lines = [`${plan.skill.displayName} (${plan.skill.id})`, ""];
103
+ for (const target of plan.targets)
104
+ lines.push(` ${target.replacing ? "replace" : "install"} → ${target.destination} [${target.displayName}]`);
105
+ return lines.join("\n");
106
+ }
107
+ /** Which first-party skills are already present for at least one agent. */
108
+ export async function installedFirstPartySkills(detected) {
109
+ const agents = (detected ?? (await detectAgents())).filter((agent) => agent.installed);
110
+ const found = new Set();
111
+ for (const skill of FIRST_PARTY_SKILLS)
112
+ for (const agent of agents)
113
+ if (await directoryExists(join(agent.skillsDirectory, skill.id))) {
114
+ found.add(skill.id);
115
+ break;
116
+ }
117
+ return found;
118
+ }
@@ -1,7 +1,7 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
- import { writeFileAtomically } from "./atomic-file.js";
4
+ import { writeFileAtomically } from "../install/atomic-file.js";
5
5
  const HANDOFF_DIR = ".handoff";
6
6
  const MESSAGES_FILE = "messages.jsonl";
7
7
  const PROTOCOL_FILE = "PROTOCOL.md";
@@ -53,8 +53,17 @@ export async function initHandoff(projectRoot) {
53
53
  "loadout handoff done <message-id>",
54
54
  "```",
55
55
  "",
56
- "Agents read this directory on wake. Loadout manages the message log;",
57
- "each agent's own CLAUDE.md / AGENTS.md can reference .handoff/ for pickup.",
56
+ "## Making agents read this",
57
+ "",
58
+ "A message log only helps if the other agent checks it. Run:",
59
+ "",
60
+ "```",
61
+ "loadout handoff pickup --yes",
62
+ "```",
63
+ "",
64
+ "That adds a managed block to CLAUDE.md and AGENTS.md telling each agent to",
65
+ "run `loadout handoff inbox <agent>` at session start and act on what it finds.",
66
+ "Only the block between the loadout:handoff markers is managed.",
58
67
  "",
59
68
  ].join("\n");
60
69
  await writeFileAtomically(join(dir, PROTOCOL_FILE), protocol);
@@ -131,6 +140,114 @@ export async function getHandoffState(projectRoot) {
131
140
  done,
132
141
  };
133
142
  }
143
+ // ---------------------------------------------------------------------------
144
+ // Consumption — the half that makes a handoff more than an outbox
145
+ // ---------------------------------------------------------------------------
146
+ /** Pending tasks addressed to one agent, oldest first. */
147
+ export async function readInbox(projectRoot, agent) {
148
+ const state = await getHandoffState(projectRoot);
149
+ return state.pending.filter((m) => m.to === agent);
150
+ }
151
+ /**
152
+ * Render an agent's inbox as instructions the agent itself can act on. This is
153
+ * what `loadout handoff inbox` prints, and what the generated pickup block
154
+ * tells each agent to run, so the message log is consumed rather than merely
155
+ * written.
156
+ */
157
+ export function formatInbox(agent, messages) {
158
+ if (!messages.length)
159
+ return `No pending handoff tasks for ${agent}.`;
160
+ const lines = [
161
+ `${messages.length} pending handoff task(s) for ${agent}:`,
162
+ "",
163
+ ];
164
+ for (const m of messages) {
165
+ lines.push(`[${m.id}] from ${m.from} (${m.timestamp.slice(0, 16).replace("T", " ")})`);
166
+ lines.push(` ${m.description}`);
167
+ if (m.context)
168
+ lines.push(` context: ${m.context}`);
169
+ lines.push(` when finished: loadout handoff done ${m.id}`);
170
+ lines.push("");
171
+ }
172
+ lines.push("Work these in order. Mark each done as you complete it so the sender sees progress.");
173
+ return lines.join("\n");
174
+ }
175
+ const PICKUP_START = "<!-- loadout:handoff:start -->";
176
+ const PICKUP_END = "<!-- loadout:handoff:end -->";
177
+ /** The managed instruction block written into an agent's context file. */
178
+ export function pickupBlock(agent) {
179
+ return [
180
+ PICKUP_START,
181
+ "",
182
+ "## Handoff inbox",
183
+ "",
184
+ `At the start of a session, and whenever you finish a task, run:`,
185
+ "",
186
+ "```bash",
187
+ `loadout handoff inbox ${agent}`,
188
+ "```",
189
+ "",
190
+ "If it lists pending tasks, work them in order and run the `loadout handoff done`",
191
+ "command it prints for each one. If it reports none, continue as normal.",
192
+ "",
193
+ PICKUP_END,
194
+ ].join("\n");
195
+ }
196
+ /** Agent context files, relative to the project root. */
197
+ const AGENT_CONTEXT_FILES = {
198
+ "claude-code": "CLAUDE.md",
199
+ codex: "AGENTS.md",
200
+ };
201
+ export function agentContextFile(agent) {
202
+ const file = AGENT_CONTEXT_FILES[agent];
203
+ if (!file)
204
+ throw new Error(`No known context file for agent '${agent}'. Supported: ${Object.keys(AGENT_CONTEXT_FILES).join(", ")}`);
205
+ return file;
206
+ }
207
+ /**
208
+ * Compute the new content for an agent's context file with the pickup block
209
+ * added or refreshed. Existing content is preserved; only the managed block
210
+ * between the markers is replaced.
211
+ */
212
+ export async function planPickup(projectRoot, agent) {
213
+ const file = agentContextFile(agent);
214
+ const path = join(projectRoot, file);
215
+ const block = pickupBlock(agent);
216
+ let existing = "";
217
+ let exists = false;
218
+ try {
219
+ existing = await readFile(path, "utf8");
220
+ exists = true;
221
+ }
222
+ catch {
223
+ // A missing context file is created with just the managed block.
224
+ }
225
+ const start = existing.indexOf(PICKUP_START);
226
+ const end = existing.indexOf(PICKUP_END);
227
+ const replacing = start !== -1 && end !== -1 && end > start;
228
+ const content = replacing
229
+ ? existing.slice(0, start) + block + existing.slice(end + PICKUP_END.length)
230
+ : exists
231
+ ? `${existing.replace(/\s*$/, "")}\n\n${block}\n`
232
+ : `${block}\n`;
233
+ return { path, agent, exists, replacing, content };
234
+ }
235
+ export async function applyPickup(plan) {
236
+ await writeFileAtomically(plan.path, plan.content);
237
+ }
238
+ export function formatPickupPlan(plans) {
239
+ const lines = ["Handoff pickup instructions:", ""];
240
+ for (const plan of plans) {
241
+ const action = plan.replacing
242
+ ? "refresh managed block in"
243
+ : plan.exists
244
+ ? "append managed block to"
245
+ : "create";
246
+ lines.push(` ${action} ${plan.path}`);
247
+ }
248
+ lines.push("", "This teaches each agent to check its handoff inbox at session start.", "Only the block between the loadout:handoff markers is managed; the rest of", "each file is preserved.");
249
+ return lines.join("\n");
250
+ }
134
251
  export function formatHandoffStatus(state) {
135
252
  if (!state.initialized)
136
253
  return "Handoff not initialized. Run `loadout handoff init`.";