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,10 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
- import { formatSchemaError, providerModelConfigurationSchema, } from "../shared/schemas.js";
5
- import { writeFileAtomically } from "./atomic-file.js";
6
- import { ensureDirectory, loadoutHome } from "./paths.js";
7
- import { runMutationTransaction } from "./transaction.js";
4
+ import { formatSchemaError, providerModelConfigurationSchema, } from "../../shared/schemas.js";
5
+ import { writeFileAtomically } from "../install/atomic-file.js";
6
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
7
+ import { runMutationTransaction } from "../install/transaction.js";
8
8
  export const defaultModelConfigurationPath = () => join(loadoutHome(), "models.json");
9
9
  /**
10
10
  * Validate a portable, provider-neutral model configuration. This is not a
@@ -4,29 +4,209 @@
4
4
  // Prices as of August 2026.
5
5
  export const MODEL_CATALOG = [
6
6
  // --- Anthropic (Claude Code) ---
7
- { id: "claude-fable-5", provider: "anthropic", name: "Claude Fable 5", tier: "frontier", inputCostPer1M: 10, outputCostPer1M: 50, nativeAgents: ["claude-code"], current: true, generation: "5" },
8
- { id: "claude-opus-5", provider: "anthropic", name: "Claude Opus 5", tier: "frontier", inputCostPer1M: 5, outputCostPer1M: 25, nativeAgents: ["claude-code"], current: true, generation: "5" },
9
- { id: "claude-sonnet-5", provider: "anthropic", name: "Claude Sonnet 5", tier: "standard", inputCostPer1M: 3, outputCostPer1M: 15, nativeAgents: ["claude-code"], current: true, generation: "5" },
10
- { id: "claude-opus-4-8", provider: "anthropic", name: "Claude Opus 4.8", tier: "frontier", inputCostPer1M: 5, outputCostPer1M: 25, nativeAgents: ["claude-code"], current: false, generation: "4" },
11
- { id: "claude-opus-4-6", provider: "anthropic", name: "Claude Opus 4.6", tier: "frontier", inputCostPer1M: 5, outputCostPer1M: 25, nativeAgents: ["claude-code"], current: false, generation: "4" },
12
- { id: "claude-sonnet-4-6", provider: "anthropic", name: "Claude Sonnet 4.6", tier: "standard", inputCostPer1M: 3, outputCostPer1M: 15, nativeAgents: ["claude-code"], current: false, generation: "4" },
13
- { id: "claude-haiku-4-5", provider: "anthropic", name: "Claude Haiku 4.5", tier: "fast", inputCostPer1M: 1, outputCostPer1M: 5, nativeAgents: ["claude-code"], current: true, generation: "4" },
7
+ {
8
+ id: "claude-fable-5",
9
+ provider: "anthropic",
10
+ name: "Claude Fable 5",
11
+ tier: "frontier",
12
+ inputCostPer1M: 10,
13
+ outputCostPer1M: 50,
14
+ nativeAgents: ["claude-code"],
15
+ current: true,
16
+ generation: "5",
17
+ },
18
+ {
19
+ id: "claude-opus-5",
20
+ provider: "anthropic",
21
+ name: "Claude Opus 5",
22
+ tier: "frontier",
23
+ inputCostPer1M: 5,
24
+ outputCostPer1M: 25,
25
+ nativeAgents: ["claude-code"],
26
+ current: true,
27
+ generation: "5",
28
+ },
29
+ {
30
+ id: "claude-sonnet-5",
31
+ provider: "anthropic",
32
+ name: "Claude Sonnet 5",
33
+ tier: "standard",
34
+ inputCostPer1M: 3,
35
+ outputCostPer1M: 15,
36
+ nativeAgents: ["claude-code"],
37
+ current: true,
38
+ generation: "5",
39
+ },
40
+ {
41
+ id: "claude-opus-4-8",
42
+ provider: "anthropic",
43
+ name: "Claude Opus 4.8",
44
+ tier: "frontier",
45
+ inputCostPer1M: 5,
46
+ outputCostPer1M: 25,
47
+ nativeAgents: ["claude-code"],
48
+ current: false,
49
+ generation: "4",
50
+ },
51
+ {
52
+ id: "claude-opus-4-6",
53
+ provider: "anthropic",
54
+ name: "Claude Opus 4.6",
55
+ tier: "frontier",
56
+ inputCostPer1M: 5,
57
+ outputCostPer1M: 25,
58
+ nativeAgents: ["claude-code"],
59
+ current: false,
60
+ generation: "4",
61
+ },
62
+ {
63
+ id: "claude-sonnet-4-6",
64
+ provider: "anthropic",
65
+ name: "Claude Sonnet 4.6",
66
+ tier: "standard",
67
+ inputCostPer1M: 3,
68
+ outputCostPer1M: 15,
69
+ nativeAgents: ["claude-code"],
70
+ current: false,
71
+ generation: "4",
72
+ },
73
+ {
74
+ id: "claude-haiku-4-5",
75
+ provider: "anthropic",
76
+ name: "Claude Haiku 4.5",
77
+ tier: "fast",
78
+ inputCostPer1M: 1,
79
+ outputCostPer1M: 5,
80
+ nativeAgents: ["claude-code"],
81
+ current: true,
82
+ generation: "4",
83
+ },
14
84
  // --- OpenAI (Codex) — GPT-5.6 family ---
15
- { id: "gpt-5.6-sol", provider: "openai", name: "GPT-5.6 Sol", tier: "frontier", inputCostPer1M: 5, outputCostPer1M: 30, nativeAgents: ["codex"], current: true, generation: "5.6" },
16
- { id: "gpt-5.6-terra", provider: "openai", name: "GPT-5.6 Terra", tier: "standard", inputCostPer1M: 2, outputCostPer1M: 12, nativeAgents: ["codex"], current: true, generation: "5.6" },
17
- { id: "gpt-5.6-luna", provider: "openai", name: "GPT-5.6 Luna", tier: "fast", inputCostPer1M: 0.20, outputCostPer1M: 1.20, nativeAgents: ["codex"], current: true, generation: "5.6" },
85
+ {
86
+ id: "gpt-5.6-sol",
87
+ provider: "openai",
88
+ name: "GPT-5.6 Sol",
89
+ tier: "frontier",
90
+ inputCostPer1M: 5,
91
+ outputCostPer1M: 30,
92
+ nativeAgents: ["codex"],
93
+ current: true,
94
+ generation: "5.6",
95
+ },
96
+ {
97
+ id: "gpt-5.6-terra",
98
+ provider: "openai",
99
+ name: "GPT-5.6 Terra",
100
+ tier: "standard",
101
+ inputCostPer1M: 2,
102
+ outputCostPer1M: 12,
103
+ nativeAgents: ["codex"],
104
+ current: true,
105
+ generation: "5.6",
106
+ },
107
+ {
108
+ id: "gpt-5.6-luna",
109
+ provider: "openai",
110
+ name: "GPT-5.6 Luna",
111
+ tier: "fast",
112
+ inputCostPer1M: 0.2,
113
+ outputCostPer1M: 1.2,
114
+ nativeAgents: ["codex"],
115
+ current: true,
116
+ generation: "5.6",
117
+ },
18
118
  // --- OpenAI — GPT-5.5 ---
19
- { id: "gpt-5.5", provider: "openai", name: "GPT-5.5", tier: "frontier", inputCostPer1M: 5, outputCostPer1M: 30, nativeAgents: ["codex"], current: true, generation: "5.5" },
119
+ {
120
+ id: "gpt-5.5",
121
+ provider: "openai",
122
+ name: "GPT-5.5",
123
+ tier: "frontier",
124
+ inputCostPer1M: 5,
125
+ outputCostPer1M: 30,
126
+ nativeAgents: ["codex"],
127
+ current: true,
128
+ generation: "5.5",
129
+ },
20
130
  // --- OpenAI — GPT-5.4 family ---
21
- { id: "gpt-5.4", provider: "openai", name: "GPT-5.4", tier: "standard", inputCostPer1M: 2.50, outputCostPer1M: 15, nativeAgents: ["codex"], current: true, generation: "5.4" },
22
- { id: "gpt-5.4-mini", provider: "openai", name: "GPT-5.4 Mini", tier: "fast", inputCostPer1M: 0.75, outputCostPer1M: 4.50, nativeAgents: ["codex"], current: true, generation: "5.4" },
131
+ {
132
+ id: "gpt-5.4",
133
+ provider: "openai",
134
+ name: "GPT-5.4",
135
+ tier: "standard",
136
+ inputCostPer1M: 2.5,
137
+ outputCostPer1M: 15,
138
+ nativeAgents: ["codex"],
139
+ current: true,
140
+ generation: "5.4",
141
+ },
142
+ {
143
+ id: "gpt-5.4-mini",
144
+ provider: "openai",
145
+ name: "GPT-5.4 Mini",
146
+ tier: "fast",
147
+ inputCostPer1M: 0.75,
148
+ outputCostPer1M: 4.5,
149
+ nativeAgents: ["codex"],
150
+ current: true,
151
+ generation: "5.4",
152
+ },
23
153
  // --- OpenAI — Codex-specific ---
24
- { id: "gpt-5.1-codex", provider: "openai", name: "GPT-5.1 Codex", tier: "standard", inputCostPer1M: 1.25, outputCostPer1M: 10, nativeAgents: ["codex"], current: true, generation: "5.1" },
154
+ {
155
+ id: "gpt-5.1-codex",
156
+ provider: "openai",
157
+ name: "GPT-5.1 Codex",
158
+ tier: "standard",
159
+ inputCostPer1M: 1.25,
160
+ outputCostPer1M: 10,
161
+ nativeAgents: ["codex"],
162
+ current: true,
163
+ generation: "5.1",
164
+ },
25
165
  // --- OpenAI — o-series reasoning ---
26
- { id: "o3-pro", provider: "openai", name: "o3-pro", tier: "frontier", inputCostPer1M: 20, outputCostPer1M: 80, nativeAgents: ["codex"], current: true, generation: "o3" },
27
- { id: "o3", provider: "openai", name: "o3", tier: "frontier", inputCostPer1M: 2, outputCostPer1M: 8, nativeAgents: ["codex"], current: true, generation: "o3" },
28
- { id: "o4-mini", provider: "openai", name: "o4-mini", tier: "standard", inputCostPer1M: 1.10, outputCostPer1M: 4.40, nativeAgents: ["codex"], current: true, generation: "o4" },
29
- { id: "o3-mini", provider: "openai", name: "o3-mini", tier: "standard", inputCostPer1M: 1.10, outputCostPer1M: 4.40, nativeAgents: ["codex"], current: true, generation: "o3" },
166
+ {
167
+ id: "o3-pro",
168
+ provider: "openai",
169
+ name: "o3-pro",
170
+ tier: "frontier",
171
+ inputCostPer1M: 20,
172
+ outputCostPer1M: 80,
173
+ nativeAgents: ["codex"],
174
+ current: true,
175
+ generation: "o3",
176
+ },
177
+ {
178
+ id: "o3",
179
+ provider: "openai",
180
+ name: "o3",
181
+ tier: "frontier",
182
+ inputCostPer1M: 2,
183
+ outputCostPer1M: 8,
184
+ nativeAgents: ["codex"],
185
+ current: true,
186
+ generation: "o3",
187
+ },
188
+ {
189
+ id: "o4-mini",
190
+ provider: "openai",
191
+ name: "o4-mini",
192
+ tier: "standard",
193
+ inputCostPer1M: 1.1,
194
+ outputCostPer1M: 4.4,
195
+ nativeAgents: ["codex"],
196
+ current: true,
197
+ generation: "o4",
198
+ },
199
+ {
200
+ id: "o3-mini",
201
+ provider: "openai",
202
+ name: "o3-mini",
203
+ tier: "standard",
204
+ inputCostPer1M: 1.1,
205
+ outputCostPer1M: 4.4,
206
+ nativeAgents: ["codex"],
207
+ current: true,
208
+ generation: "o3",
209
+ },
30
210
  ];
31
211
  const TIER_LABELS = {
32
212
  frontier: "Frontier (deep reasoning)",
@@ -74,12 +254,70 @@ const PHASE_CONFIG = {
74
254
  },
75
255
  };
76
256
  const PHASE_KEYWORDS = {
77
- plan: ["plan", "design", "architect", "architecture", "rfc", "spec", "decompose", "strategy", "system design"],
78
- implement: ["implement", "build", "code", "create", "add", "feature", "write", "develop", "make"],
79
- review: ["review", "audit", "check", "inspect", "critique", "pr", "pull request", "diff"],
80
- test: ["test", "spec", "coverage", "unit", "integration", "e2e", "assert", "vitest", "jest"],
81
- debug: ["debug", "fix", "bug", "error", "crash", "broken", "investigate", "troubleshoot", "failing"],
82
- document: ["document", "docs", "readme", "comment", "jsdoc", "changelog", "explain", "docstring"],
257
+ plan: [
258
+ "plan",
259
+ "design",
260
+ "architect",
261
+ "architecture",
262
+ "rfc",
263
+ "spec",
264
+ "decompose",
265
+ "strategy",
266
+ "system design",
267
+ ],
268
+ implement: [
269
+ "implement",
270
+ "build",
271
+ "code",
272
+ "create",
273
+ "add",
274
+ "feature",
275
+ "write",
276
+ "develop",
277
+ "make",
278
+ ],
279
+ review: [
280
+ "review",
281
+ "audit",
282
+ "check",
283
+ "inspect",
284
+ "critique",
285
+ "pr",
286
+ "pull request",
287
+ "diff",
288
+ ],
289
+ test: [
290
+ "test",
291
+ "spec",
292
+ "coverage",
293
+ "unit",
294
+ "integration",
295
+ "e2e",
296
+ "assert",
297
+ "vitest",
298
+ "jest",
299
+ ],
300
+ debug: [
301
+ "debug",
302
+ "fix",
303
+ "bug",
304
+ "error",
305
+ "crash",
306
+ "broken",
307
+ "investigate",
308
+ "troubleshoot",
309
+ "failing",
310
+ ],
311
+ document: [
312
+ "document",
313
+ "docs",
314
+ "readme",
315
+ "comment",
316
+ "jsdoc",
317
+ "changelog",
318
+ "explain",
319
+ "docstring",
320
+ ],
83
321
  };
84
322
  // ---------------------------------------------------------------------------
85
323
  // Classification
@@ -114,6 +352,25 @@ export function cheapestInTier(tier) {
114
352
  const models = modelsForTier(tier);
115
353
  return models.sort((a, b) => a.inputCostPer1M - b.inputCostPer1M)[0];
116
354
  }
355
+ /**
356
+ * Narrow a recommendation to the agents actually present on this machine. A
357
+ * recommendation that names an agent the user has not installed is noise, so
358
+ * the resolved list drives both the printed advice and the handoff command.
359
+ */
360
+ export function resolveAvailableAgents(suggested, installed) {
361
+ const present = new Set(installed);
362
+ return {
363
+ available: suggested.filter((id) => present.has(id)),
364
+ missing: suggested.filter((id) => !present.has(id)),
365
+ };
366
+ }
367
+ /** Models in the recommended tier that the given agents can actually run. */
368
+ export function modelsRunnableBy(models, agents) {
369
+ if (!agents.length)
370
+ return models;
371
+ const wanted = new Set(agents);
372
+ return models.filter((m) => m.nativeAgents.some((id) => wanted.has(id)));
373
+ }
117
374
  // ---------------------------------------------------------------------------
118
375
  // Routing
119
376
  // ---------------------------------------------------------------------------
@@ -166,16 +423,29 @@ export function estimateCostSavings() {
166
423
  const conserveTotal = conserveEstimates.reduce((s, c) => s + c, 0);
167
424
  return { estimates, normalTotal, conserveTotal };
168
425
  }
169
- // ---------------------------------------------------------------------------
170
- // Formatters
171
- // ---------------------------------------------------------------------------
172
- export function formatRouteRecommendation(rec) {
173
- const modelNames = rec.models.slice(0, 4).map((m) => `${m.name} ($${m.inputCostPer1M}/$${m.outputCostPer1M})`);
426
+ /** Shell-quote a task description for the suggested handoff command. */
427
+ function shellQuote(value) {
428
+ return `'${value.replace(/'/g, `'\\''`)}'`;
429
+ }
430
+ export function formatRouteRecommendation(rec, context = {}) {
431
+ // `undefined` means detection never ran, so every suggested agent stands. An
432
+ // empty array means detection ran and found nothing, which must not be
433
+ // silently upgraded back to the full suggestion list.
434
+ const checked = context.installedAgents !== undefined;
435
+ const { available, missing } = checked
436
+ ? resolveAvailableAgents(rec.suggestedAgents, context.installedAgents)
437
+ : { available: rec.suggestedAgents, missing: [] };
438
+ // Only recommend models an available agent can actually run.
439
+ const runnable = checked
440
+ ? modelsRunnableBy(rec.models, available)
441
+ : rec.models;
442
+ const shown = (runnable.length ? runnable : rec.models).slice(0, 4);
443
+ const modelNames = shown.map((m) => `${m.name} ($${m.inputCostPer1M}/$${m.outputCostPer1M})`);
174
444
  const lines = [
175
445
  `Phase: ${rec.phase}`,
176
446
  `Tier: ${rec.tierLabel}`,
177
447
  `Models: ${modelNames.join("\n ")}`,
178
- `Agents: ${rec.suggestedAgents.join(", ")}`,
448
+ `Agents: ${available.length ? available.join(", ") : "none detected"}${missing.length ? ` (not installed: ${missing.join(", ")})` : ""}`,
179
449
  `Why: ${rec.reason}`,
180
450
  ];
181
451
  if (rec.conserveAlternative) {
@@ -183,6 +453,13 @@ export function formatRouteRecommendation(rec) {
183
453
  const altCheapest = cheapestInTier(alt.tier);
184
454
  lines.push(``, `Conserve: drop to ${alt.tier} tier (${altCheapest?.name ?? "—"} at $${altCheapest?.inputCostPer1M ?? "?"}/$${altCheapest?.outputCostPer1M ?? "?"})`, ` ${alt.tradeoff}`);
185
455
  }
456
+ // Actionable next step: hand this task to a detected agent.
457
+ if (context.description && available.length) {
458
+ const target = available[0];
459
+ lines.push(``, `Hand off:`, ` loadout handoff send ${target} ${shellQuote(context.description)}`);
460
+ if (context.handoffReady === false)
461
+ lines.push(` (run \`loadout handoff init\` once to enable)`);
462
+ }
186
463
  return lines.join("\n");
187
464
  }
188
465
  export function formatRoutingTable(conserve = false) {
@@ -200,8 +477,13 @@ export function formatRoutingTable(conserve = false) {
200
477
  return `${phase} ${tier} ${model} ${inCost} ${outCost} ${agents}`;
201
478
  });
202
479
  return [
203
- conserve ? "ROUTING TABLE (conserve mode — usage-saving tier for each phase)" : "ROUTING TABLE",
204
- "", header, divider, ...rows,
480
+ conserve
481
+ ? "ROUTING TABLE (conserve mode — usage-saving tier for each phase)"
482
+ : "ROUTING TABLE",
483
+ "",
484
+ header,
485
+ divider,
486
+ ...rows,
205
487
  ].join("\n");
206
488
  }
207
489
  export function formatCostTable() {
@@ -216,7 +498,10 @@ export function formatCostTable() {
216
498
  });
217
499
  const savings = Math.round((1 - conserveTotal / normalTotal) * 100);
218
500
  return [
219
- header, divider, ...rows, divider,
501
+ header,
502
+ divider,
503
+ ...rows,
504
+ divider,
220
505
  `Normal total: $${normalTotal.toFixed(2)}/M input across 6 phases`,
221
506
  `Conserve total: $${conserveTotal.toFixed(2)}/M input (${savings}% cheaper)`,
222
507
  "",
@@ -244,5 +529,11 @@ export function formatModelCatalog(filter) {
244
529
  const agents = m.nativeAgents.slice(0, 3).join(", ");
245
530
  return `${name} ${provider} ${tier} ${inCost} ${outCost} ${gen} ${agents}`;
246
531
  });
247
- return [`${models.length} models${filter?.current === false ? " (including legacy)" : ""}`, "", header, divider, ...rows].join("\n");
532
+ return [
533
+ `${models.length} models${filter?.current === false ? " (including legacy)" : ""}`,
534
+ "",
535
+ header,
536
+ divider,
537
+ ...rows,
538
+ ].join("\n");
248
539
  }
@@ -1,8 +1,8 @@
1
1
  import { createServer, } from "node:http";
2
- import { loadEffectiveCatalog, rankCatalog } from "./catalog.js";
3
- import { detectAgents } from "./paths.js";
4
- import { buildUpdatePlan } from "./update.js";
5
- import { buildHealthReport } from "./health.js";
2
+ import { loadEffectiveCatalog, rankCatalog } from "../catalog/catalog.js";
3
+ import { detectAgents } from "../agents/paths.js";
4
+ import { buildUpdatePlan } from "../install/update.js";
5
+ import { buildHealthReport } from "../reporting/health.js";
6
6
  function json(response, statusCode, body) {
7
7
  const payload = JSON.stringify(body);
8
8
  response.writeHead(statusCode, {
@@ -1,4 +1,4 @@
1
- import { evaluatePackage } from "./evaluate.js";
1
+ import { evaluatePackage, } from "../discovery/evaluate.js";
2
2
  /**
3
3
  * Run the policy gate for a candidate without mutating agent configuration.
4
4
  * Promotion is deliberately injected so callers can connect it to their
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
- import { loadoutHome } from "./paths.js";
4
+ import { loadoutHome } from "../agents/paths.js";
5
5
  function normalizeRepository(input) {
6
6
  const value = input
7
7
  .trim()
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
- import { createCredentialResolver } from "./credentials.js";
4
+ import { createCredentialResolver } from "../routing/credentials.js";
5
5
  import { planMcpConfig } from "./mcp.js";
6
6
  /**
7
7
  * Small, source-linked recipes. These configure a connection only; authorizing
@@ -1,8 +1,8 @@
1
1
  import { readFile, readdir, mkdir, rename, writeFile, rm, } from "node:fs/promises";
2
2
  import { join, resolve, dirname, basename } from "node:path";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { loadoutHome } from "./paths.js";
5
- import { runMutationTransaction, withMutationLock } from "./transaction.js";
4
+ import { loadoutHome } from "../agents/paths.js";
5
+ import { runMutationTransaction, withMutationLock, } from "../install/transaction.js";
6
6
  const missingConfigMarker = "loadout:mcp-config:missing";
7
7
  async function configFingerprint(path) {
8
8
  let content;
@@ -2,10 +2,10 @@ import { execFile } from "node:child_process";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { dirname, win32, join } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import { writeFileAtomically } from "./atomic-file.js";
6
- import { ensureDirectory, loadoutHome, userHome } from "./paths.js";
7
- import { createSnapshot, recordSnapshotPostMutationState } from "./snapshot.js";
8
- import { beginTransaction, completeTransaction, markTransactionCommitting, recoverPendingTransactions, rollbackTransaction, } from "./transaction.js";
5
+ import { writeFileAtomically } from "../install/atomic-file.js";
6
+ import { ensureDirectory, loadoutHome, userHome } from "../agents/paths.js";
7
+ import { createSnapshot, recordSnapshotPostMutationState, } from "../install/snapshot.js";
8
+ import { beginTransaction, completeTransaction, markTransactionCommitting, recoverPendingTransactions, rollbackTransaction, } from "../install/transaction.js";
9
9
  const execFileAsync = promisify(execFile);
10
10
  function parseTime(value) {
11
11
  const match = /^(\d{2}):(\d{2})$/.exec(value);
@@ -1,8 +1,8 @@
1
1
  import { applyActivationChange, formatActivationPlan, planActivationChange, } from "./active-set.js";
2
2
  import { DEFAULT_ACTIVE_SKILL_LIMIT } from "./active-limit.js";
3
- import { detectAgents } from "./paths.js";
3
+ import { detectAgents } from "../agents/paths.js";
4
4
  import { formatDetectedSignals, scanProject } from "./recommend.js";
5
- import { scanInstalledSkills } from "./skill-inventory.js";
5
+ import { scanInstalledSkills } from "../catalog/skill-inventory.js";
6
6
  import { readInstallState } from "./state.js";
7
7
  import { outcomeAdjustment, projectTaskFamilies, readLocalOutcomes, } from "./outcomes.js";
8
8
  const FOUNDATION = new Set([
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { cp, lstat, mkdir, readFile, readdir, rm } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
- import { agentSkillsDirectory, ensureDirectory } from "./paths.js";
4
+ import { agentSkillsDirectory, ensureDirectory } from "../agents/paths.js";
5
5
  import { activationLibraryPath, installStatePath, readInstallState, writeInstallState, } from "./state.js";
6
6
  import { inspectTargetOccupancy } from "./target-occupancy.js";
7
- import { runMutationTransaction } from "./transaction.js";
7
+ import { runMutationTransaction } from "../install/transaction.js";
8
8
  /** Resolve a managed file to its active location or disabled library copy. */
9
9
  export function managedFileReadPath(packageId, filePath, activations) {
10
10
  const disabled = activations.find((activation) => activation.packageId === packageId &&
@@ -1,9 +1,9 @@
1
- import { buildHealthReport } from "./health.js";
1
+ import { buildHealthReport } from "../reporting/health.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { join, resolve } from "node:path";
5
- import { loadoutHome } from "./paths.js";
6
- import { detectSecretKinds } from "./safety.js";
5
+ import { loadoutHome } from "../agents/paths.js";
6
+ import { detectSecretKinds } from "../catalog/safety.js";
7
7
  export function proposeImprovements(report) {
8
8
  const proposals = [];
9
9
  if (report.driftedFiles)
@@ -1,8 +1,8 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
- import { formatSchemaError, loadoutManifestSchema } from "../shared/schemas.js";
3
+ import { formatSchemaError, loadoutManifestSchema, } from "../../shared/schemas.js";
4
4
  import { readInstallState } from "./state.js";
5
- import { writeFileAtomically } from "./atomic-file.js";
5
+ import { writeFileAtomically } from "../install/atomic-file.js";
6
6
  const AGENTS = new Set([
7
7
  "claude-code",
8
8
  "codex",
@@ -1,9 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { dirname, join } from "node:path";
4
- import { writeFileAtomically } from "./atomic-file.js";
5
- import { ensureDirectory, loadoutHome } from "./paths.js";
6
- import { withFileLock } from "./file-lock.js";
4
+ import { writeFileAtomically } from "../install/atomic-file.js";
5
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
6
+ import { withFileLock } from "../install/file-lock.js";
7
7
  const outcomePath = () => join(loadoutHome(), "outcomes.json");
8
8
  const outcomeLockPath = () => join(loadoutHome(), "outcomes.lock");
9
9
  const RESULTS = new Set([
@@ -1,9 +1,9 @@
1
1
  import { lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
- import { parseLockfile, readLockfile } from "./audit.js";
3
+ import { parseLockfile, readLockfile } from "../catalog/audit.js";
4
4
  import { parseManifest, readManifest } from "./manifest.js";
5
- import { runMutationTransaction } from "./transaction.js";
6
- import { detectSecretKinds } from "./safety.js";
5
+ import { runMutationTransaction } from "../install/transaction.js";
6
+ import { detectSecretKinds } from "../catalog/safety.js";
7
7
  export function parsePortableLoadout(value) {
8
8
  if (!value || typeof value !== "object" || Array.isArray(value))
9
9
  throw new Error("Portable Loadout must be an object");
@@ -1,4 +1,4 @@
1
- import { resolveCatalogProfile } from "./profiles.js";
1
+ import { resolveCatalogProfile } from "../catalog/profiles.js";
2
2
  import { readInstallState, writeInstallState } from "./state.js";
3
3
  export async function recordInstalledProfile(prepared) {
4
4
  const profile = {
@@ -1,6 +1,6 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { basename, resolve } from "node:path";
3
- import { POWER_SKILL_ALLOWLIST, STABLE_BOOST_PACKAGE_IDS } from "./profiles.js";
3
+ import { POWER_SKILL_ALLOWLIST, STABLE_BOOST_PACKAGE_IDS, } from "../catalog/profiles.js";
4
4
  import { packageOutcomeAdjustment, projectTaskFamilies, } from "./outcomes.js";
5
5
  const SIGNAL_FILES = new Set([
6
6
  "package.json",
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, readdir, stat } from "node:fs/promises";
3
3
  import { basename, join, relative, sep } from "node:path";
4
- import { formatSchemaError, installStateSchema } from "../shared/schemas.js";
5
- import { ensureDirectory, loadoutHome } from "./paths.js";
6
- import { writeFileAtomically } from "./atomic-file.js";
4
+ import { formatSchemaError, installStateSchema } from "../../shared/schemas.js";
5
+ import { ensureDirectory, loadoutHome } from "../agents/paths.js";
6
+ import { writeFileAtomically } from "../install/atomic-file.js";
7
7
  const stateFile = () => join(loadoutHome(), "state.json");
8
8
  export const installStatePath = () => stateFile();
9
9
  export async function writeInstallState(state) {