loadout-ai 0.4.1 → 0.5.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 (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/MASTER_PLAN.md +106 -33
  3. package/README.md +57 -19
  4. package/catalog/discovered.json +8422 -6957
  5. package/catalog/packages.json +26 -0
  6. package/dist/src/cli.js +140 -68
  7. package/dist/src/core/active-policy.js +48 -8
  8. package/dist/src/core/active-set.js +1 -1
  9. package/dist/src/core/adopt.js +1 -1
  10. package/dist/src/core/catalog-coverage.js +2 -1
  11. package/dist/src/core/catalog-install.js +23 -8
  12. package/dist/src/core/cli-guide.js +5 -2
  13. package/dist/src/core/codex-mcp.js +49 -5
  14. package/dist/src/core/completion.js +0 -3
  15. package/dist/src/core/health.js +19 -11
  16. package/dist/src/core/install.js +3 -3
  17. package/dist/src/core/mcp-recipes.js +13 -4
  18. package/dist/src/core/mcp.js +6 -5
  19. package/dist/src/core/model-config.js +1 -1
  20. package/dist/src/core/portable.js +1 -1
  21. package/dist/src/core/recommend.js +34 -18
  22. package/dist/src/core/remove.js +28 -2
  23. package/dist/src/core/runtime-tool-recipe.js +2 -2
  24. package/dist/src/core/runtime-tools.js +15 -4
  25. package/dist/src/core/snapshot.js +20 -0
  26. package/dist/src/core/state.js +9 -0
  27. package/dist/src/core/sync.js +1 -1
  28. package/dist/src/core/transaction.js +2 -1
  29. package/dist/src/shared/schemas.js +1 -0
  30. package/docs/CATALOG.md +3 -2
  31. package/docs/DISCOVERED.md +253 -255
  32. package/docs/FEATURE_TEST_MATRIX.md +19 -32
  33. package/docs/GITHUB_AUTHORIZATION.md +2 -2
  34. package/docs/TESTING.md +3 -16
  35. package/docs/USER_TEST_GUIDE.md +23 -9
  36. package/docs/assets/loadout-workflow.png +0 -0
  37. package/docs/evidence/readme-claims.json +0 -8
  38. package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +116 -0
  39. package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +55 -0
  40. package/package.json +4 -6
  41. package/dashboard/app.js +0 -607
  42. package/dashboard/index.html +0 -249
  43. package/dashboard/styles.css +0 -384
  44. package/dist/src/core/demo.js +0 -136
  45. package/dist/src/dashboard.js +0 -421
@@ -1146,5 +1146,31 @@
1146
1146
  "evidencePaths": ["server.js", "package.json"],
1147
1147
  "verifiedAt": "2026-07-16T08:45:18Z"
1148
1148
  }
1149
+ },
1150
+ {
1151
+ "id": "obsidian-skills",
1152
+ "displayName": "Obsidian Skills",
1153
+ "repository": "kepano/obsidian-skills",
1154
+ "description": "Agent skills for working with Obsidian Markdown, Bases, JSON Canvas, Defuddle, and the Obsidian CLI.",
1155
+ "category": "knowledge-management",
1156
+ "tier": "community",
1157
+ "license": "MIT",
1158
+ "components": ["skill", "plugin"],
1159
+ "operatingSystems": ["windows", "macos", "linux"],
1160
+ "stars": 42827,
1161
+ "pushedAt": "2026-06-08T16:12:01Z",
1162
+ "source": {
1163
+ "type": "github",
1164
+ "url": "https://github.com/kepano/obsidian-skills",
1165
+ "defaultBranch": "main",
1166
+ "commit": "a1dc48e68138490d522c04cbf5822214c6eb1202",
1167
+ "evidencePaths": [
1168
+ "skills/obsidian-markdown/SKILL.md",
1169
+ "skills/obsidian-bases/SKILL.md",
1170
+ "skills/json-canvas/SKILL.md",
1171
+ ".claude-plugin/plugin.json"
1172
+ ],
1173
+ "verifiedAt": "2026-07-21T08:45:00Z"
1174
+ }
1149
1175
  }
1150
1176
  ]
package/dist/src/cli.js CHANGED
@@ -3,21 +3,21 @@ import { Command, CommanderError } from "commander";
3
3
  import { existsSync } from "node:fs";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { explainCatalogScore, loadEffectiveCatalog, loadCatalog, rankCatalog, refreshCatalog, } from "./core/catalog.js";
6
- import { detectAgents, parseAgentSelection } from "./core/paths.js";
6
+ import { detectAgents, parseAgentSelection, userHome } from "./core/paths.js";
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { buildSkillPlan, applySkillInstall, installedAgents, } from "./core/install.js";
10
- import { listSnapshotIds, readSnapshot, restoreSnapshot, } from "./core/snapshot.js";
10
+ import { listSnapshotIds, readSnapshot, restoreSnapshot, summarizeSnapshot, } from "./core/snapshot.js";
11
11
  import { fetchRepositorySnapshot } from "./core/source.js";
12
12
  import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "./core/mcp.js";
13
- import { REVIEWED_MCP_RECIPES, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
13
+ import { REVIEWED_MCP_RECIPES, buildMcpRecipeServer, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
14
14
  import { runDoctor, formatDoctorReport } from "./core/doctor.js";
15
15
  import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, selectSafeAutomaticUpdates, } from "./core/update.js";
16
16
  import { startApiServer } from "./core/api.js";
17
17
  import { inspectPackage, formatPackageInspection } from "./core/package.js";
18
18
  import { addManifestPackage, applyProfileToManifest, initManifest, readManifest, removeManifestPackage, writeLockfile, } from "./core/manifest.js";
19
19
  import { buildHealthReport, formatHealthReport } from "./core/health.js";
20
- import { readInstallState, recordInstallTransaction } from "./core/state.js";
20
+ import { installStatePath, readInstallState, recordInstallTransaction, recordMcpInstall, } from "./core/state.js";
21
21
  import { applyRemove, planRemove } from "./core/remove.js";
22
22
  import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "./core/uninstall.js";
23
23
  import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "./core/profile-state.js";
@@ -31,8 +31,7 @@ import { ADAPTER_CAPABILITIES, buildAdapterCapabilityGaps, formatAdapterCapabili
31
31
  import { formatAgentInventory, inspectAgents, } from "./core/agent-inspection.js";
32
32
  import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signing.js";
33
33
  import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
34
- import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
35
- import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
34
+ import { applyCodexMcpConfigPlan, codexMcpServerFingerprint, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
36
35
  import { catalogTrustStage, formatCatalogTrustStage, resolveCatalogProfile, } from "./core/profiles.js";
37
36
  import { discoverHackerNewsRepositories } from "./core/community.js";
38
37
  import { discoverPrivateRepositories } from "./core/private-discovery.js";
@@ -44,7 +43,6 @@ import { runDisposableSandbox } from "./core/sandbox.js";
44
43
  import { compileConversion, } from "./core/conversion.js";
45
44
  import { writeFileAtomically } from "./core/atomic-file.js";
46
45
  import { formatCanaryResult, runCanary } from "./core/canary.js";
47
- import { startDashboardServer } from "./dashboard.js";
48
46
  import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
49
47
  import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
50
48
  import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
@@ -222,7 +220,7 @@ async function runSetup(options) {
222
220
  onProgress: printSetupProgress,
223
221
  access,
224
222
  });
225
- console.log(`\n${formatPreparedCatalogInstall(prepared)}\n`);
223
+ console.log(`\n${formatPreparedCatalogInstall(prepared, { details: options.details })}\n`);
226
224
  const risky = riskyPackageSummary(prepared);
227
225
  let approved = Boolean(options.yes);
228
226
  let riskApproved = Boolean(options.approveRisk);
@@ -265,7 +263,7 @@ async function runSetup(options) {
265
263
  reader?.close();
266
264
  }
267
265
  }
268
- const LOADOUT_VERSION = "0.4.1";
266
+ const LOADOUT_VERSION = "0.5.0";
269
267
  function durableSchedulerLauncher() {
270
268
  return [
271
269
  join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
@@ -308,6 +306,7 @@ program
308
306
  .option("--api-access <providers>", "separately billed model API access: none, openai, anthropic, openrouter, or other (comma-separated; never a key)")
309
307
  .option("-y, --yes", "install after preparing the screened plan")
310
308
  .option("--approve-risk", "approve reviewed safety findings in non-interactive mode")
309
+ .option("--details", "show every quarantined and deferred unit")
311
310
  .action((options) => runSetup(options));
312
311
  program
313
312
  .command("upgrade")
@@ -1077,6 +1076,12 @@ program
1077
1076
  : Object.entries(TESTED_PROFILES)
1078
1077
  .map(([id, profile]) => `${id} — ${profile.description}`)
1079
1078
  .join("\n"));
1079
+ if (name === "custom" && !options.applyTo) {
1080
+ const profile = TESTED_PROFILES.custom;
1081
+ return console.log(options.json
1082
+ ? JSON.stringify({ name, ...profile, packages: [] }, null, 2)
1083
+ : `custom: ${profile.description}\n Use: loadout setup --mode custom --package <id>`);
1084
+ }
1080
1085
  const packages = profileManifestPackages(name, await loadEffectiveCatalog());
1081
1086
  if (options.applyTo) {
1082
1087
  const manifest = await applyProfileToManifest(options.applyTo, name, packages);
@@ -1465,26 +1470,6 @@ benchmark
1465
1470
  if (!summary.withinBudget)
1466
1471
  process.exitCode = 2;
1467
1472
  });
1468
- program
1469
- .command("demo")
1470
- .alias("test-drive")
1471
- .description("Test-drive a reviewed install + rollback in a disposable profile; never touches local agent config")
1472
- .option("--repository <owner/repo>", "public GitHub skill repository", "obra/superpowers")
1473
- .option("--package <id>", "package identifier", "obra-superpowers")
1474
- .option("--agents <ids>", "comma-separated virtual demo targets", "codex")
1475
- .option("--keep", "retain the isolated profile after install for inspection")
1476
- .option("--json", "emit the demo result as JSON")
1477
- .action(async (options) => {
1478
- const result = await runIsolatedDemo({
1479
- repository: options.repository,
1480
- packageId: options.package,
1481
- agents: parseAgentSelection(options.agents),
1482
- keep: options.keep,
1483
- });
1484
- console.log(options.json
1485
- ? JSON.stringify(result, null, 2)
1486
- : formatDemoResult(result));
1487
- });
1488
1473
  program
1489
1474
  .command("capabilities")
1490
1475
  .description("Show honest native, adapted, and unsupported adapter capabilities")
@@ -2150,6 +2135,7 @@ program
2150
2135
  .command("mcp-recipe")
2151
2136
  .description("Preview/configure a reviewed MCP recipe, or explicitly verify its real connection")
2152
2137
  .argument("[id]", "recipe id; omit to list reviewed recipes")
2138
+ .option("--agent <id>", "target host: codex or claude-code; selects its default config format and path")
2153
2139
  .option("--config <path>", "target JSON MCP config path")
2154
2140
  .option("--yes", "write the reviewed server entry after preview")
2155
2141
  .option("--verify", "verify the configured entry without starting a server")
@@ -2176,11 +2162,14 @@ program
2176
2162
  environment: recipe.environment,
2177
2163
  modelApiProviders: recipe.modelApiProviders,
2178
2164
  }));
2179
- console.log(options.json
2165
+ const output = options.json
2180
2166
  ? JSON.stringify(listed, null, 2)
2181
2167
  : listed
2182
2168
  .map((recipe) => `${recipe.id} — ${recipe.displayName} — ${recipe.modelApiProviders.length ? `AI API required: ${recipe.modelApiProviders.join(", ")}` : "No AI API key required"} · ${recipe.environment.length ? `${recipe.environment.includes("GITHUB_PERSONAL_ACCESS_TOKEN") ? "GitHub token required" : `service credential required: ${recipe.environment.join(", ")}`}` : "no other credential"}`)
2183
- .join("\n"));
2169
+ .join("\n");
2170
+ console.log(output);
2171
+ if (!options.json)
2172
+ console.log("\nInstall for one host (preview first):\n loadout mcp-recipe playwright --agent codex\n loadout mcp-recipe playwright --agent claude-code");
2184
2173
  return;
2185
2174
  }
2186
2175
  if (options.connect) {
@@ -2210,12 +2199,41 @@ program
2210
2199
  }
2211
2200
  return;
2212
2201
  }
2213
- if (!options.config)
2214
- throw new Error("--config is required for recipe planning or --verify");
2202
+ if (options.agent &&
2203
+ options.agent !== "codex" &&
2204
+ options.agent !== "claude-code")
2205
+ throw new Error("--agent must be codex or claude-code");
2206
+ if (options.agent && options.config)
2207
+ throw new Error("Choose --agent for a managed default path or --config for an explicit JSON path, not both");
2208
+ const configPath = options.config ??
2209
+ (options.agent === "codex"
2210
+ ? defaultCodexMcpConfigPath()
2211
+ : options.agent === "claude-code"
2212
+ ? join(userHome(), ".claude.json")
2213
+ : undefined);
2214
+ if (!configPath)
2215
+ throw new Error("Choose --agent codex, --agent claude-code, or an explicit --config path");
2215
2216
  if (options.verify) {
2216
2217
  if (options.yes)
2217
2218
  throw new Error("--verify cannot be combined with --yes");
2218
- const verification = await verifyMcpRecipe(id, options.config);
2219
+ if (options.agent === "codex") {
2220
+ const recipe = findMcpRecipe(id);
2221
+ let content = "";
2222
+ try {
2223
+ content = await readFile(configPath, "utf8");
2224
+ }
2225
+ catch {
2226
+ // Missing config is rendered as a normal not-configured result.
2227
+ }
2228
+ const configured = Boolean(codexMcpServerFingerprint(content, recipe.serverName));
2229
+ console.log(options.json
2230
+ ? JSON.stringify({ recipeId: id, configPath, configured }, null, 2)
2231
+ : `${configured ? "Configured" : "Not configured"}: ${id}\nCodex config: ${configPath}\nConfiguration presence does not launch the MCP server; add --connect --approve-risk for a bounded handshake.`);
2232
+ if (!configured)
2233
+ process.exitCode = 1;
2234
+ return;
2235
+ }
2236
+ const verification = await verifyMcpRecipe(id, configPath);
2219
2237
  console.log(options.json
2220
2238
  ? JSON.stringify(verification, null, 2)
2221
2239
  : `${verification.configured ? "Configured" : "Not configured"}: ${verification.recipeId}\n${[...verification.checks, ...verification.warnings].join("\n")}`);
@@ -2225,21 +2243,82 @@ program
2225
2243
  }
2226
2244
  const recipe = findMcpRecipe(id);
2227
2245
  const credentialReferences = parseMcpCredentialMappings(recipe, options.credential, options.credentialAccount);
2228
- const plan = await planMcpRecipe(id, options.config, {
2246
+ if (options.agent === "codex") {
2247
+ const server = buildMcpRecipeServer(id, configPath, {
2248
+ credentialReferences,
2249
+ requireResolvedCredentials: Boolean(options.yes),
2250
+ });
2251
+ const plan = await planCodexMcpConfig(configPath, server, server.name);
2252
+ const recipe = findMcpRecipe(id);
2253
+ const preview = [
2254
+ `MCP recipe: ${recipe.displayName}`,
2255
+ `Target: Codex`,
2256
+ `Config: ${plan.path}`,
2257
+ `Change: ${plan.summary}`,
2258
+ `Artifact: ${recipe.artifact}`,
2259
+ `Permissions: ${recipe.permissions.join(", ")}`,
2260
+ recipe.environment.length
2261
+ ? `Credential references required: ${recipe.environment.join(", ")}`
2262
+ : "Credentials: none",
2263
+ ].join("\n");
2264
+ if (!options.yes) {
2265
+ console.log(options.json
2266
+ ? JSON.stringify({
2267
+ recipe,
2268
+ agent: "codex",
2269
+ path: plan.path,
2270
+ summary: plan.summary,
2271
+ }, null, 2)
2272
+ : `${preview}\nDry run only. Re-run with --yes to add this reviewed Codex MCP server.`);
2273
+ return;
2274
+ }
2275
+ const fingerprint = codexMcpServerFingerprint(plan.proposed, recipe.serverName);
2276
+ if (!fingerprint)
2277
+ throw new Error("Could not fingerprint the applied Codex MCP entry");
2278
+ const snapshot = await applyCodexMcpConfigPlan(plan, {
2279
+ extraTargets: [installStatePath()],
2280
+ afterApply: async (_result, transactionSnapshot) => recordMcpInstall({
2281
+ packageId: `mcp-recipe:${id}:codex`,
2282
+ configPath: plan.path,
2283
+ serverName: recipe.serverName,
2284
+ fingerprint,
2285
+ snapshotId: transactionSnapshot.id,
2286
+ installedAt: new Date().toISOString(),
2287
+ configFormat: "codex-toml",
2288
+ }),
2289
+ });
2290
+ console.log(options.json
2291
+ ? JSON.stringify({
2292
+ recipe,
2293
+ agent: "codex",
2294
+ snapshotId: snapshot.rollbackSnapshotId,
2295
+ }, null, 2)
2296
+ : `${preview}\nConfigured for Codex. Managed package: mcp-recipe:${id}:codex\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nNext: loadout mcp-recipe ${id} --agent codex --verify`);
2297
+ return;
2298
+ }
2299
+ const plan = await planMcpRecipe(id, configPath, {
2229
2300
  credentialReferences,
2230
2301
  requireResolvedCredentials: Boolean(options.yes),
2231
2302
  });
2232
2303
  if (!options.yes) {
2304
+ const target = options.agent === "claude-code" ? "Target: Claude Code\n" : "";
2233
2305
  console.log(options.json
2234
- ? JSON.stringify(plan, null, 2)
2235
- : `${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
2306
+ ? JSON.stringify(options.agent ? { agent: options.agent, ...plan } : plan, null, 2)
2307
+ : `${target}${formatMcpRecipePlan(plan)}\nDry run only. Re-run with --yes to write this server entry.`);
2236
2308
  return;
2237
2309
  }
2238
- const snapshot = await applyMcpConfigPlan(plan.config);
2239
- await recordInstallTransaction([], [{ packageId: `mcp-recipe:${id}`, plan: plan.config }], snapshot.id);
2310
+ const managedId = options.agent
2311
+ ? `mcp-recipe:${id}:${options.agent}`
2312
+ : `mcp-recipe:${id}`;
2313
+ const snapshot = await applyMcpConfigPlan(plan.config, {
2314
+ extraTargets: [installStatePath()],
2315
+ afterApply: async (_result, transactionSnapshot) => {
2316
+ await recordInstallTransaction([], [{ packageId: managedId, plan: plan.config }], transactionSnapshot.id);
2317
+ },
2318
+ });
2240
2319
  console.log(options.json
2241
2320
  ? JSON.stringify({ plan, snapshot }, null, 2)
2242
- : `${formatMcpRecipePlan(plan)}\nConfigured. Snapshot: ${snapshot.id}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} --config ${plan.config.path} --verify`);
2321
+ : `${options.agent === "claude-code" ? "Target: Claude Code\n" : ""}${formatMcpRecipePlan(plan)}\nConfigured${options.agent ? " for Claude Code" : ""}. Managed package: ${managedId}\nRollback: loadout rollback --snapshot ${snapshot.rollbackSnapshotId}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} ${options.agent ? `--agent ${options.agent}` : `--config ${plan.config.path}`} --verify`);
2243
2322
  });
2244
2323
  program
2245
2324
  .command("mcp")
@@ -2439,7 +2518,7 @@ program
2439
2518
  return;
2440
2519
  }
2441
2520
  const snapshot = await applyMcpConfigPlan(plan);
2442
- console.log(`Applied successfully. Snapshot: ${snapshot.id}`);
2521
+ console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
2443
2522
  });
2444
2523
  program
2445
2524
  .command("codex-mcp-config")
@@ -2478,7 +2557,7 @@ program
2478
2557
  if (!options.yes)
2479
2558
  return console.log("Dry run only. Re-run with --yes to add this Codex MCP server.");
2480
2559
  const snapshot = await applyCodexMcpConfigPlan(plan);
2481
- console.log(`Applied successfully. Snapshot: ${snapshot.id}`);
2560
+ console.log(`Applied successfully. Snapshot: ${snapshot.rollbackSnapshotId}`);
2482
2561
  });
2483
2562
  program
2484
2563
  .command("plan")
@@ -2575,6 +2654,7 @@ program
2575
2654
  .option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
2576
2655
  .option("--yes", "apply without interactive confirmation")
2577
2656
  .option("--approve-risk", "approve reviewed safety findings for catalog mode")
2657
+ .option("--details", "show every quarantined and deferred unit")
2578
2658
  .action(async (options) => {
2579
2659
  const packageIds = options.package ?? [];
2580
2660
  const hasSource = Boolean(options.source || options.repository);
@@ -2589,7 +2669,9 @@ program
2589
2669
  requestedAgents: parseAgentSelection(options.agents),
2590
2670
  onProgress: printSetupProgress,
2591
2671
  });
2592
- console.log(formatPreparedCatalogInstall(prepared));
2672
+ console.log(formatPreparedCatalogInstall(prepared, {
2673
+ details: options.details,
2674
+ }));
2593
2675
  if (!options.yes) {
2594
2676
  console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
2595
2677
  return;
@@ -2662,8 +2744,15 @@ program
2662
2744
  const snapshotIds = await listSnapshotIds();
2663
2745
  if (!snapshotIds.length)
2664
2746
  return console.log("No Loadout snapshots found.");
2665
- for (const id of snapshotIds)
2666
- console.log(id);
2747
+ console.log("Loadout rollback history (oldest to newest):");
2748
+ for (const [index, id] of snapshotIds.entries()) {
2749
+ const summary = summarizeSnapshot(await readSnapshot(id));
2750
+ const impact = summary.changedEntries === undefined
2751
+ ? "legacy impact unknown"
2752
+ : `${summary.changedEntries} changed filesystem entr${summary.changedEntries === 1 ? "y" : "ies"}`;
2753
+ console.log(`${index === snapshotIds.length - 1 ? "latest" : " "} ${summary.createdAt} ${summary.label} ${impact} ${summary.roots} root(s) ${id}`);
2754
+ }
2755
+ console.log("Restore an exact entry with: loadout rollback --snapshot <id>");
2667
2756
  return;
2668
2757
  }
2669
2758
  const selected = await withMutationLock(async () => {
@@ -2671,12 +2760,15 @@ program
2671
2760
  const chosen = options.snapshot ?? snapshotIds.at(-1);
2672
2761
  if (!chosen)
2673
2762
  throw new Error("No Loadout snapshots found");
2674
- await restoreSnapshot(await readSnapshot(chosen), {
2763
+ const snapshot = await readSnapshot(chosen);
2764
+ await restoreSnapshot(snapshot, {
2675
2765
  requireUnchangedPostMutationState: true,
2676
2766
  });
2677
- return chosen;
2767
+ return summarizeSnapshot(snapshot);
2678
2768
  });
2679
- console.log(`Restored snapshot ${selected}`);
2769
+ console.log(`Restored ${selected.label} snapshot ${selected.id} (${selected.changedEntries ?? "unknown"} recorded changed filesystem entries across ${selected.roots} root(s)).`);
2770
+ if (selected.changedEntries === 0)
2771
+ console.log("This snapshot recorded no effective filesystem change. Choose an older explicit snapshot if you meant to undo an earlier installation.");
2680
2772
  });
2681
2773
  program
2682
2774
  .command("update")
@@ -2844,26 +2936,6 @@ program
2844
2936
  if (result.status === "blocked")
2845
2937
  process.exitCode = 1;
2846
2938
  });
2847
- program
2848
- .command("dashboard")
2849
- .description("Start the local Loadout dashboard on a loopback-only port")
2850
- .option("--port <port>", "TCP port (0 selects an available port)", "0")
2851
- .action(async (options) => {
2852
- const port = Number(options.port);
2853
- if (!Number.isInteger(port) || port < 0 || port > 65_535)
2854
- throw new Error("--port must be an integer between 0 and 65535");
2855
- const handle = await startDashboardServer({}, port);
2856
- console.log(`Loadout dashboard: http://${handle.host}:${handle.port}`);
2857
- await new Promise((resolve) => {
2858
- const stop = () => {
2859
- process.off("SIGINT", stop);
2860
- process.off("SIGTERM", stop);
2861
- void handle.close().then(resolve);
2862
- };
2863
- process.on("SIGINT", stop);
2864
- process.on("SIGTERM", stop);
2865
- });
2866
- });
2867
2939
  program
2868
2940
  .command("serve")
2869
2941
  .description("Start a loopback-only read-only API for status, health, catalog, and updates")
@@ -19,13 +19,46 @@ const FOUNDATION = new Set([
19
19
  "code-review-excellence",
20
20
  "api-design-principles",
21
21
  "architecture-patterns",
22
- "accessibility-compliance",
23
22
  "deployment-pipeline-design",
24
23
  "documentation-writer",
25
24
  "openai-docs",
26
- "web-design-guidelines",
27
25
  ]);
28
- const SPECIALIZED_WITHOUT_SIGNAL = /(?:appstore|backtesting|bats-|oracle|salesforce|power-bi|dataverse|qdrant|azure-|aws-|mcp-server-generator|sandbox-npm|migration)/;
26
+ const SPECIALIZED_WITHOUT_SIGNAL = /(?:appstore|msstore|backtesting|bats-|oracle|salesforce|power-bi|dataverse|qdrant|azure-|aws-|mcp-server-generator|sandbox-npm|migration|social-(?:publish|media)|marketing|copywriting)/;
27
+ /**
28
+ * A matching word such as `mcp`, `cli`, or `publish` is not enough to make a
29
+ * skill compatible with the detected project. These gates run before scoring
30
+ * so a strong generic signal cannot pull in tooling for another ecosystem.
31
+ * Explicit pins remain an intentional user override.
32
+ */
33
+ function hasProjectCompatibility(name, project) {
34
+ const hasLanguage = (language) => project.languages.includes(language);
35
+ const languageRequirements = [
36
+ [/(?:dotnet|csharp|aspnet|nuget|maui)/, ".net"],
37
+ [
38
+ /(?:python|pytest|fastapi|django|flask|pydantic|poetry|uv-package)/,
39
+ "python",
40
+ ],
41
+ [/(?:^|-)go(?:-|$)|golang/, "go"],
42
+ [/(?:rust|cargo)/, "rust"],
43
+ [/(?:^|-)java(?:-|$)|spring-boot/, "java"],
44
+ [/(?:ruby|rails|bundler)/, "ruby"],
45
+ [/(?:elixir|phoenix)/, "elixir"],
46
+ ];
47
+ for (const [pattern, language] of languageRequirements)
48
+ if (pattern.test(name) && !hasLanguage(language))
49
+ return false;
50
+ if (/(?:nodejs-)?backend|api-server/.test(name) &&
51
+ !project.roles.includes("backend"))
52
+ return false;
53
+ if (/(?:^|-)vercel(?:-|$)/.test(name) &&
54
+ !project.frameworks.includes("next.js") &&
55
+ !project.files.includes("vercel.json"))
56
+ return false;
57
+ if (/(?:publish-to-pages|github-pages|cloudflare-pages)/.test(name) &&
58
+ !project.frameworks.some((framework) => ["react", "next.js", "vue", "svelte"].includes(framework)))
59
+ return false;
60
+ return true;
61
+ }
29
62
  const SOURCE_PRIORITY = {
30
63
  superpowers: 80,
31
64
  context7: 75,
@@ -49,7 +82,7 @@ const GENERAL = [
49
82
  const SIGNAL_RULES = [
50
83
  {
51
84
  signal: (project) => project.languages.includes("javascript/typescript"),
52
- pattern: /(?:javascript|typescript|nodejs|npm|webapp)/,
85
+ pattern: /(?:javascript|typescript|nodejs|webapp)|(?:^|-)npm(?:-|$)/,
53
86
  label: "JavaScript/TypeScript project",
54
87
  },
55
88
  {
@@ -89,12 +122,12 @@ const SIGNAL_RULES = [
89
122
  },
90
123
  {
91
124
  signal: (project) => project.roles.includes("node-cli"),
92
- pattern: /(?:^|-)cli(?:-|$)|command-line/,
125
+ pattern: /^(?:cli|cli-design|cli-mastery|node-cli|command-line|command-line-tools?|building-cli(?:-apps?)?)$/,
93
126
  label: "Node CLI",
94
127
  },
95
128
  {
96
129
  signal: (project) => project.roles.includes("npm-package"),
97
- pattern: /npm|package|publish/,
130
+ pattern: /(?:^|-)npm(?:-|$)|package|publish/,
98
131
  label: "npm package",
99
132
  },
100
133
  {
@@ -112,6 +145,11 @@ const SIGNAL_RULES = [
112
145
  pattern: /security|threat|secrets|owasp/,
113
146
  label: "security policy",
114
147
  },
148
+ {
149
+ signal: (project) => project.roles.includes("obsidian-vault"),
150
+ pattern: /obsidian|json-canvas|defuddle/,
151
+ label: "Obsidian vault",
152
+ },
115
153
  {
116
154
  signal: (project) => project.tools.includes("vitest"),
117
155
  pattern: /vitest/,
@@ -119,12 +157,12 @@ const SIGNAL_RULES = [
119
157
  },
120
158
  {
121
159
  signal: (project) => project.tools.includes("commander"),
122
- pattern: /(?:^|-)cli(?:-|$)|commander|command-line/,
160
+ pattern: /^(?:cli|cli-design|cli-mastery|node-cli|commander|command-line|command-line-tools?|building-cli(?:-apps?)?)$/,
123
161
  label: "Commander CLI",
124
162
  },
125
163
  {
126
164
  signal: (project) => project.tools.includes("zod"),
127
- pattern: /zod|schema|validation/,
165
+ pattern: /zod|schema-validation|(?:^|-)validation(?:-|$)/,
128
166
  label: "Zod schemas",
129
167
  },
130
168
  ];
@@ -179,6 +217,8 @@ function scoreCandidate(record, project, pinned, outcomes, taskFamilies) {
179
217
  reasons.push("explicitly pinned");
180
218
  }
181
219
  const explicitlyPinned = score >= 1000;
220
+ if (!explicitlyPinned && !hasProjectCompatibility(name, project))
221
+ return undefined;
182
222
  if (!explicitlyPinned && SPECIALIZED_WITHOUT_SIGNAL.test(name))
183
223
  return undefined;
184
224
  if (FOUNDATION.has(name)) {
@@ -367,7 +367,7 @@ export async function applyActivationChange(plan, options = {}) {
367
367
  }
368
368
  state.activations = records.map((record) => replacements.get(activationKey(record)) ?? record);
369
369
  await writeInstallState(state);
370
- });
370
+ }, { label: `${plan.action} project skill set` });
371
371
  return applied.snapshotId;
372
372
  }
373
373
  function formatLibraryHeadline(report) {
@@ -208,7 +208,7 @@ export async function applySkillAdoption(plan, options = {}) {
208
208
  throw new Error("The skill changed after preview; scan again before adopting it.");
209
209
  },
210
210
  });
211
- });
211
+ }, { label: `adopt ${plan.packageId}` });
212
212
  return applied.snapshotId;
213
213
  }
214
214
  export function formatAdoptionPlan(plan) {
@@ -77,7 +77,8 @@ export function formatCatalogCoverage(report) {
77
77
  .join(", ");
78
78
  return [
79
79
  `Screened catalog: ${report.technicallyScreenedRecords}/${report.records} technically complete records (target ${report.targetRecords})`,
80
- `Policy selection: ${report.recommendedRecords} Stable sources · ${report.trustStages["human-reviewed"]} human-reviewed · ${report.trustStages.benchmarked} benchmarked`,
80
+ `Catalog maturity: ${report.records} sourced · ${report.technicallyScreenedRecords} technically inspected · ${report.recommendedRecords} selected for Stable`,
81
+ `Higher-confidence evidence: ${report.trustStages["human-reviewed"]} stored human-review attestations · ${report.trustStages.benchmarked} signed benchmark results`,
81
82
  `Evidence: ${report.immutablePins} immutable pins · ${report.assertedLicenses} asserted licenses · ${report.noAssertionLicenses} NOASSERTION`,
82
83
  `Coverage: ${Object.values(report.categories).length} categories · ${report.evaluationReady} evaluation-ready · ${report.activityObserved} with refreshed activity`,
83
84
  `Install shape: ${report.installShapes.skills} skill · ${report.installShapes.mcpOnly} MCP-only · ${report.installShapes.mixed} mixed`,
@@ -123,14 +123,17 @@ export async function prepareCatalogInstall(selection, options = {}) {
123
123
  catch (error) {
124
124
  completed += 1;
125
125
  const reason = error instanceof Error ? error.message : String(error);
126
+ const fullyQuarantined = rejected.size > 0 && /No SKILL\.md found/.test(reason);
126
127
  options.onProgress?.({
127
128
  packageId: pkg.id,
128
129
  completed,
129
130
  total: installable.length,
130
131
  status: "skipped",
131
- message: `${pkg.displayName} could not be prepared: ${reason}`,
132
+ message: fullyQuarantined
133
+ ? `${pkg.displayName}: all ${rejected.size} discovered skill unit(s) were quarantined`
134
+ : `${pkg.displayName} could not be prepared: ${reason}`,
132
135
  });
133
- if (rejected.size > 0 && /No SKILL\.md found/.test(reason))
136
+ if (fullyQuarantined)
134
137
  return { quarantined: [...rejected.values()] };
135
138
  return {
136
139
  result: {
@@ -195,7 +198,7 @@ export async function prepareCatalogInstall(selection, options = {}) {
195
198
  reconciliation,
196
199
  };
197
200
  }
198
- export function formatPreparedCatalogInstall(prepared) {
201
+ export function formatPreparedCatalogInstall(prepared, options = {}) {
199
202
  const targetDirectories = prepared.entries.reduce((total, entry) => total + entry.plan.files.length, 0);
200
203
  const directoriesPerAgent = prepared.agents.length
201
204
  ? Math.ceil(targetDirectories / prepared.agents.length)
@@ -215,11 +218,13 @@ export function formatPreparedCatalogInstall(prepared) {
215
218
  "Automatic skill setup does not require an OpenAI, Anthropic, or OpenRouter API key; credentialed MCP/runtime integrations remain explicit and deferred.",
216
219
  ];
217
220
  if (directoriesPerAgent > RECOMMENDED_ACTIVE_SKILL_LIMIT)
218
- lines.push(`Capacity warning: about ${directoriesPerAgent} skill directories per agent exceeds the recommended active-set limit of ${RECOMMENDED_ACTIVE_SKILL_LIMIT}.${prepared.selection.mode === "maximum" ? " Maximum stores these in the disabled library; use project activation to choose the working set." : " Prefer Stable or project-aware activation for smaller context."}`);
221
+ lines.push(`Capacity notice: about ${directoriesPerAgent} skill directories per agent exceeds Stable's ${RECOMMENDED_ACTIVE_SKILL_LIMIT}-skill bound.${prepared.selection.mode === "maximum" ? " Maximum stores them in the disabled library; optimize or activate a project-relevant working set." : prepared.selection.mode === "power" ? " Power is the explicit larger active mode; choose Stable or project optimization when lower context use matters." : " Use project-aware activation for a smaller working set."}`);
219
222
  if (failures.length)
220
223
  lines.push(`Preparation failures (installation will remain blocked): ${failures.map((item) => item.packageId).join(", ")}`);
221
224
  if (quarantined.length)
222
225
  lines.push(`Quarantined invalid skill units: ${quarantined.length} (safe siblings remain available)`);
226
+ if (explicit.length)
227
+ lines.push(`Deferred integration setup: ${explicit.length} record(s) remain opt-in through MCP/runtime commands.`);
223
228
  if (prepared.collisions.length)
224
229
  lines.push(`Overlapping skill targets resolved: ${prepared.collisions.length} lower-ranked duplicate directories deferred`);
225
230
  if (retired.length) {
@@ -228,11 +233,19 @@ export function formatPreparedCatalogInstall(prepared) {
228
233
  lines.push(`Retire ${item.packageId}${item.unitId ? `/${item.unitId}` : ""} from ${item.agent}`);
229
234
  }
230
235
  if (risky.length)
231
- lines.push(`Additional risk approval required: ${risky.map((entry) => entry.package.id).join(", ")}`);
236
+ lines.push(options.details
237
+ ? `Additional risk approval required: ${risky.map((entry) => entry.package.id).join(", ")}`
238
+ : `Additional risk approval required for ${risky.length} repositor${risky.length === 1 ? "y" : "ies"}. Run the preview with --details before approval.`);
232
239
  for (const warning of prepared.resolution.warnings)
233
240
  lines.push(`Warning: ${warning}`);
234
- for (const item of prepared.skipped)
235
- lines.push(`${item.kind === "preparation-failed" ? "Failed" : item.kind === "quarantined" ? "Quarantined" : "Deferred"} ${item.packageId}${item.unitId ? `/${item.unitId}` : ""}: ${item.reason}`);
241
+ if (failures.length)
242
+ for (const item of failures)
243
+ lines.push(`Failed ${item.packageId}${item.unitId ? `/${item.unitId}` : ""}: ${item.reason}`);
244
+ if (options.details)
245
+ for (const item of prepared.skipped.filter((item) => item.kind !== "preparation-failed"))
246
+ lines.push(`${item.kind === "quarantined" ? "Quarantined" : "Deferred"} ${item.packageId}${item.unitId ? `/${item.unitId}` : ""}: ${item.reason}`);
247
+ else if (quarantined.length || explicit.length)
248
+ lines.push("Use --details to show every quarantined or deferred unit.");
236
249
  return lines.join("\n");
237
250
  }
238
251
  export async function applyPreparedCatalogInstall(prepared, options = {}) {
@@ -258,5 +271,7 @@ export async function applyPreparedCatalogInstall(prepared, options = {}) {
258
271
  });
259
272
  }
260
273
  export function formatCatalogApplyGuidance(riskApprovalRequired) {
261
- return `Preview complete; nothing was changed. Re-run with --yes${riskApprovalRequired ? " --approve-risk" : ""} to install this exact screened plan.`;
274
+ return riskApprovalRequired
275
+ ? "Preview complete; nothing was changed. Inspect the plan with --details, then re-run with --yes --approve-risk only if you accept every reported finding."
276
+ : "Preview complete; nothing was changed. Re-run with --yes to install this exact screened plan.";
262
277
  }
@@ -7,6 +7,7 @@ START HERE
7
7
 
8
8
  2. Preview the recommended everyday setup
9
9
  loadout setup --mode stable
10
+ loadout profiles
10
11
 
11
12
  3. Find additions that fit the project in this folder
12
13
  loadout recommend --project .
@@ -17,8 +18,10 @@ START HERE
17
18
  loadout alerts
18
19
  loadout candidate list --limit 10
19
20
 
20
- 5. If you want a visual view, start the private local dashboard
21
- loadout dashboard
21
+ 5. See reviewed MCP integrations and their credential needs
22
+ loadout mcp-recipe
23
+ loadout mcp-recipe --credential-free
24
+ loadout mcp-recipe playwright --agent codex
22
25
 
23
26
  If you decide to install something, Loadout shows a preview first and creates a
24
27
  snapshot before changing managed files. Recover with: loadout rollback