loadout-ai 0.2.3 → 0.4.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 (49) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/MASTER_PLAN.md +141 -33
  3. package/README.md +160 -255
  4. package/dashboard/app.js +4 -4
  5. package/dashboard/index.html +4 -4
  6. package/dist/src/cli.js +189 -32
  7. package/dist/src/core/active-set.js +37 -2
  8. package/dist/src/core/adapters.js +10 -0
  9. package/dist/src/core/adopt.js +165 -32
  10. package/dist/src/core/agent-health-score.js +2 -2
  11. package/dist/src/core/catalog-coverage.js +2 -1
  12. package/dist/src/core/catalog-install.js +8 -1
  13. package/dist/src/core/catalog-release.js +2 -1
  14. package/dist/src/core/cli-guide.js +101 -0
  15. package/dist/src/core/completion.js +3 -0
  16. package/dist/src/core/conformance.js +74 -0
  17. package/dist/src/core/install.js +36 -3
  18. package/dist/src/core/mcp-recipes.js +21 -0
  19. package/dist/src/core/profile-state.js +101 -0
  20. package/dist/src/core/profiles.js +9 -4
  21. package/dist/src/core/ranking.js +1 -1
  22. package/dist/src/core/readme-claims.js +10 -0
  23. package/dist/src/core/readme-facts.js +40 -0
  24. package/dist/src/core/recommend.js +9 -3
  25. package/dist/src/core/remove.js +6 -7
  26. package/dist/src/core/runtime-tools.js +10 -2
  27. package/dist/src/core/scheduler.js +4 -3
  28. package/dist/src/core/snapshot.js +58 -13
  29. package/dist/src/core/state.js +13 -3
  30. package/dist/src/core/transaction.js +2 -1
  31. package/dist/src/core/uninstall.js +133 -0
  32. package/dist/src/core/update.js +87 -58
  33. package/dist/src/dashboard.js +5 -2
  34. package/dist/src/shared/schemas.js +69 -0
  35. package/docs/FEATURE_TEST_MATRIX.md +16 -0
  36. package/docs/README_RESEARCH.md +36 -0
  37. package/docs/RELEASE_REVIEW.md +31 -5
  38. package/docs/REPOSITORY_STABILIZATION.md +190 -0
  39. package/docs/TESTING.md +75 -2
  40. package/docs/USER_TEST_GUIDE.md +202 -0
  41. package/docs/assets/loadout-hero.svg +259 -0
  42. package/docs/assets/loadout-mark.svg +54 -0
  43. package/docs/evidence/live-checks-2026-07-19.json +22 -0
  44. package/docs/evidence/live-checks.schema.json +28 -0
  45. package/docs/evidence/readme-claims.json +286 -0
  46. package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +283 -0
  47. package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +80 -0
  48. package/package.json +8 -4
  49. package/SIMPLE_PLAN.md +0 -44
package/dist/src/cli.js CHANGED
@@ -12,14 +12,16 @@ import { fetchRepositorySnapshot } from "./core/source.js";
12
12
  import { discoverMcpManifests, summarizeMcpManifest, planMcpConfig, summarizeMcpConfigPlan, applyMcpConfigPlan, } from "./core/mcp.js";
13
13
  import { REVIEWED_MCP_RECIPES, findMcpRecipe, formatMcpRecipePlan, planMcpRecipe, verifyMcpRecipe, verifyMcpRecipeConnection, } from "./core/mcp-recipes.js";
14
14
  import { runDoctor, formatDoctorReport } from "./core/doctor.js";
15
- import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, } from "./core/update.js";
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 } from "./core/state.js";
20
+ import { readInstallState, recordInstallTransaction } from "./core/state.js";
21
21
  import { applyRemove, planRemove } from "./core/remove.js";
22
- import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
22
+ import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "./core/uninstall.js";
23
+ import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "./core/profile-state.js";
24
+ import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
23
25
  import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "./core/improve.js";
24
26
  import { applySyncPlan, buildSyncPlan } from "./core/sync.js";
25
27
  import { createPackage, packPackage, publishLocalPackage, publishRemotePackage, searchLocalRegistry, } from "./core/registry.js";
@@ -31,7 +33,7 @@ import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signi
31
33
  import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
32
34
  import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
33
35
  import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
34
- import { catalogTrustStage, resolveCatalogProfile } from "./core/profiles.js";
36
+ import { catalogTrustStage, formatCatalogTrustStage, resolveCatalogProfile, } from "./core/profiles.js";
35
37
  import { discoverHackerNewsRepositories } from "./core/community.js";
36
38
  import { discoverPrivateRepositories } from "./core/private-discovery.js";
37
39
  import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "./core/github-discovery.js";
@@ -43,11 +45,11 @@ import { compileConversion, } from "./core/conversion.js";
43
45
  import { writeFileAtomically } from "./core/atomic-file.js";
44
46
  import { formatCanaryResult, runCanary } from "./core/canary.js";
45
47
  import { startDashboardServer } from "./dashboard.js";
46
- import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
48
+ import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
47
49
  import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
48
50
  import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
49
51
  import { compareSkill, formatSkillComparison } from "./core/skill-compare.js";
50
- import { applyActivationChange, buildLibraryStateReport, formatActivationPlan, formatLibraryStateReport, planActivationChange, } from "./core/active-set.js";
52
+ import { applyActivationChange, buildLibraryStateReport, formatActivationPlan, formatLibrarySummary, formatLibraryStateReport, planActivationChange, } from "./core/active-set.js";
51
53
  import { applyProjectActivation, formatProjectActivation, planProjectActivation, } from "./core/active-policy.js";
52
54
  import { applySkillAdoption, formatAdoptionPlan, planSkillAdoption, } from "./core/adopt.js";
53
55
  import { formatReviewQueue, mergeReviewQueue, readReviewQueue, setReviewDecision, } from "./core/review-queue.js";
@@ -79,6 +81,7 @@ import { applyIntelligenceFeed, previewIntelligenceFeed, readCachedIntelligenceF
79
81
  import { scanSkillSecurity } from "./core/skill-security.js";
80
82
  import { planApmImportFiles, planOpenPackageImportFiles, } from "./core/ecosystem-import.js";
81
83
  import { buildCompatibilityIntelligence, parseCompatibilityNoticeSet, } from "./core/compatibility-intelligence.js";
84
+ import { ADVANCED_GUIDE, BEGINNER_GUIDE, HIDDEN_FROM_FIRST_SCREEN, } from "./core/cli-guide.js";
82
85
  const collectOption = (value, previous = []) => [
83
86
  ...previous,
84
87
  value,
@@ -180,14 +183,14 @@ async function runSetup(options) {
180
183
  try {
181
184
  if (!mode) {
182
185
  if (!interactive) {
183
- console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for the recommended daily driver, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
186
+ console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for Loadout's bounded policy selection, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
184
187
  return;
185
188
  }
186
189
  reader = createInterface({
187
190
  input: process.stdin,
188
191
  output: process.stdout,
189
192
  });
190
- const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (recommended), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
193
+ const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (Loadout policy selection), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
191
194
  mode =
192
195
  answer === "2"
193
196
  ? "power"
@@ -225,7 +228,7 @@ async function runSetup(options) {
225
228
  let riskApproved = Boolean(options.approveRisk);
226
229
  if (!approved) {
227
230
  if (!interactive) {
228
- console.log("Preview complete; nothing was changed. Re-run with --yes to install this exact screened plan.");
231
+ console.log(formatCatalogApplyGuidance(Boolean(risky)));
229
232
  return;
230
233
  }
231
234
  reader ??= createInterface({
@@ -262,7 +265,7 @@ async function runSetup(options) {
262
265
  reader?.close();
263
266
  }
264
267
  }
265
- const LOADOUT_VERSION = "0.2.3";
268
+ const LOADOUT_VERSION = "0.4.0";
266
269
  function durableSchedulerLauncher() {
267
270
  return [
268
271
  join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
@@ -271,6 +274,9 @@ function durableSchedulerLauncher() {
271
274
  ];
272
275
  }
273
276
  const program = new Command();
277
+ function printBeginnerGuide() {
278
+ console.log(BEGINNER_GUIDE);
279
+ }
274
280
  program
275
281
  .name("loadout")
276
282
  .description("The trusted upgrade layer for AI coding agents")
@@ -285,6 +291,14 @@ program
285
291
  // writes its own parse error first, producing two stderr documents and
286
292
  // breaking --json-errors consumers.
287
293
  .configureOutput({ writeErr: () => undefined });
294
+ program
295
+ .command("guide")
296
+ .description("Show the simple, read-only path for using Loadout")
297
+ .action(printBeginnerGuide);
298
+ program
299
+ .command("advanced")
300
+ .description("Explain where to find advanced and maintainer-only commands")
301
+ .action(() => console.log(ADVANCED_GUIDE));
288
302
  program
289
303
  .command("setup")
290
304
  .description("Preview and install a screened skill loadout for detected agents")
@@ -620,11 +634,14 @@ program
620
634
  .command("library")
621
635
  .description("Show separate cache, review, installation, and per-agent activation state")
622
636
  .option("--json", "emit machine-readable JSON")
637
+ .option("--all", "show every managed skill and its source package")
623
638
  .action(async (options) => {
624
639
  const report = await buildLibraryStateReport();
625
640
  console.log(options.json
626
641
  ? JSON.stringify(report, null, 2)
627
- : formatLibraryStateReport(report));
642
+ : options.all
643
+ ? formatLibraryStateReport(report)
644
+ : formatLibrarySummary(report));
628
645
  });
629
646
  program
630
647
  .command("report")
@@ -712,8 +729,8 @@ for (const workflow of ["activate", "optimize"]) {
712
729
  program
713
730
  .command(workflow)
714
731
  .description(workflow === "activate"
715
- ? "Select and activate reviewed library skills for a project"
716
- : "Scan a project and propose the best reviewed active-set additions")
732
+ ? "Select and activate inspected library skills for a project"
733
+ : "Scan a project and propose rule-selected inspected active-set additions")
717
734
  .option("--project <path>", "project directory to scan", ".")
718
735
  .option("--agents <ids>", "comma-separated agent ids")
719
736
  .option("--limit <count>", "maximum active skills per agent", "40")
@@ -891,7 +908,17 @@ program
891
908
  .option("--explain", "add deterministic score dimensions, evidence, uncertainty, and remediation")
892
909
  .option("--agents <ids>", "limit explained scores to selected agent ids")
893
910
  .action(async (options) => {
894
- const report = await buildHealthReport({ checkUpdates: options.updates });
911
+ if (options.updates && !options.json)
912
+ console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
913
+ const report = await buildHealthReport({
914
+ updates: options.updates
915
+ ? () => buildUpdatePlan(undefined, {
916
+ onProgress: options.json
917
+ ? undefined
918
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
919
+ })
920
+ : undefined,
921
+ });
895
922
  if (options.agents && !options.explain)
896
923
  throw new Error("--agents requires --explain");
897
924
  const selectedAgents = parseAgentSelection(options.agents);
@@ -985,6 +1012,29 @@ program
985
1012
  const snapshot = await applyRemove(plan, { force: options.force });
986
1013
  console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
987
1014
  });
1015
+ program
1016
+ .command("uninstall")
1017
+ .description("Preview or completely remove Loadout-managed data")
1018
+ .option("--yes", "remove managed data; otherwise show a preview")
1019
+ .option("--force", "also remove managed files changed outside Loadout")
1020
+ .option("--remove-cli", "also uninstall the global loadout-ai npm command")
1021
+ .action(async (options) => {
1022
+ const plan = await buildUninstallPlan();
1023
+ console.log(formatUninstallPlan(plan));
1024
+ if (!options.yes)
1025
+ return;
1026
+ const result = await applyUninstall(plan, undefined, {
1027
+ force: options.force,
1028
+ });
1029
+ console.log(`Removed ${result.removedPackages} managed package(s), ${result.removedRuntimeTools} runtime tool(s), daily jobs, cache, library, and Loadout state.`);
1030
+ if (options.removeCli) {
1031
+ await uninstallGlobalCli();
1032
+ console.log("Removed the global loadout-ai CLI. Open a new shell before checking the command again.");
1033
+ }
1034
+ else {
1035
+ console.log("The CLI is still installed. Remove it too with: npm uninstall -g loadout-ai");
1036
+ }
1037
+ });
988
1038
  program
989
1039
  .command("recommend")
990
1040
  .description("Recommend catalog packages from local project signals")
@@ -1004,6 +1054,7 @@ program
1004
1054
  ? JSON.stringify({
1005
1055
  signals,
1006
1056
  recommendations,
1057
+ recommendationBoundary: RECOMMENDATION_BOUNDARY,
1007
1058
  personalization: options.agent
1008
1059
  ? {
1009
1060
  agent: options.agent,
@@ -1015,7 +1066,7 @@ program
1015
1066
  });
1016
1067
  program
1017
1068
  .command("profiles")
1018
- .description("List tested Loadout profiles or inspect one profile")
1069
+ .description("List Loadout policy profiles or inspect one profile")
1019
1070
  .argument("[name]", "profile name")
1020
1071
  .option("--json", "emit machine-readable JSON")
1021
1072
  .option("--apply-to <path>", "add the selected profile packages to a manifest")
@@ -1132,7 +1183,7 @@ program
1132
1183
  .description("Take ownership of one explicitly selected installed skill without changing its bytes")
1133
1184
  .argument("<skill>", "installed skill name, directory name, or exact path")
1134
1185
  .requiredOption("--agent <id>", "agent that owns the installed skill")
1135
- .option("--refresh-provenance", "rebuild the reviewed catalog skill index")
1186
+ .option("--refresh-provenance", "rebuild the inspected catalog skill index")
1136
1187
  .option("--yes", "record ownership; otherwise show a dry-run plan")
1137
1188
  .option("--json", "emit machine-readable JSON")
1138
1189
  .action(async (skill, options) => {
@@ -1167,7 +1218,7 @@ program
1167
1218
  .description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
1168
1219
  .argument("<skill>", "installed skill name, directory name, or catalog package id")
1169
1220
  .option("--agent <id>", "select one installed agent when names are ambiguous")
1170
- .option("--refresh", "rebuild the reviewed catalog skill index")
1221
+ .option("--refresh", "rebuild the inspected catalog skill index")
1171
1222
  .option("--offline", "never fetch; use the existing local index only")
1172
1223
  .option("--limit <count>", "maximum alternatives to show", "10")
1173
1224
  .option("--json", "emit the complete machine-readable comparison")
@@ -1508,6 +1559,7 @@ program
1508
1559
  category: pkg.category,
1509
1560
  tier: pkg.tier,
1510
1561
  trustStage: catalogTrustStage(pkg),
1562
+ evidenceLabel: formatCatalogTrustStage(catalogTrustStage(pkg)),
1511
1563
  },
1512
1564
  ranking: explainCatalogScore(pkg),
1513
1565
  }, null, 2));
@@ -1520,12 +1572,16 @@ program
1520
1572
  : formatCatalogCoverage(coverage));
1521
1573
  return;
1522
1574
  }
1575
+ if (options.json) {
1576
+ console.log(JSON.stringify(rankCatalog(result.catalog), null, 2));
1577
+ return;
1578
+ }
1523
1579
  for (const pkg of rankCatalog(result.catalog)) {
1524
1580
  const topics = pkg.topics?.length ? ` — ${pkg.topics.join(", ")}` : "";
1525
1581
  const updated = pkg.lastUpdatedAt
1526
1582
  ? ` — updated ${pkg.lastUpdatedAt.slice(0, 10)}`
1527
1583
  : "";
1528
- console.log(`${pkg.displayName} [${pkg.tier}; ${catalogTrustStage(pkg)}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
1584
+ console.log(`${pkg.displayName} [${pkg.tier}; ${formatCatalogTrustStage(catalogTrustStage(pkg))}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
1529
1585
  }
1530
1586
  for (const failure of result.failures)
1531
1587
  console.error(`Warning: could not refresh ${failure.repository}: ${failure.error}`);
@@ -1593,7 +1649,7 @@ candidate
1593
1649
  .description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
1594
1650
  .argument("<dossier>", "persisted candidate dossier JSON")
1595
1651
  .requiredOption("--id <id>", "lowercase kebab-case catalog id")
1596
- .requiredOption("--category <category>", "reviewed catalog category")
1652
+ .requiredOption("--category <category>", "inspected catalog category")
1597
1653
  .requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
1598
1654
  .option("--display-name <name>", "reviewed display name")
1599
1655
  .option("--description <text>", "reviewed description")
@@ -2103,21 +2159,27 @@ program
2103
2159
  .option("--timeout <milliseconds>", "real connection timeout", "8000")
2104
2160
  .option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
2105
2161
  .option("--json", "emit machine-readable JSON")
2162
+ .option("--no-key", "list recipes needing no separately billed AI/model API key")
2163
+ .option("--no-model-key", "clear alias for --no-key; service tokens may still be required")
2164
+ .option("--credential-free", "list recipes needing no credential of any kind")
2106
2165
  .action(async (id, options) => {
2107
2166
  if (!id) {
2108
2167
  if (options.connect || options.verify || options.yes)
2109
2168
  throw new Error("Select an MCP recipe id for this operation");
2110
- const listed = REVIEWED_MCP_RECIPES.map((recipe) => ({
2169
+ const selected = REVIEWED_MCP_RECIPES.filter((recipe) => !((options.key === false || options.modelKey === false) &&
2170
+ recipe.modelApiProviders.length > 0) && !(options.credentialFree && recipe.environment.length > 0));
2171
+ const listed = selected.map((recipe) => ({
2111
2172
  id: recipe.id,
2112
2173
  displayName: recipe.displayName,
2113
2174
  source: recipe.source,
2114
2175
  permissions: recipe.permissions,
2115
2176
  environment: recipe.environment,
2177
+ modelApiProviders: recipe.modelApiProviders,
2116
2178
  }));
2117
2179
  console.log(options.json
2118
2180
  ? JSON.stringify(listed, null, 2)
2119
2181
  : listed
2120
- .map((recipe) => `${recipe.id} — ${recipe.displayName} — env: ${recipe.environment.length ? recipe.environment.join(", ") : "none"}`)
2182
+ .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"}`)
2121
2183
  .join("\n"));
2122
2184
  return;
2123
2185
  }
@@ -2174,6 +2236,7 @@ program
2174
2236
  return;
2175
2237
  }
2176
2238
  const snapshot = await applyMcpConfigPlan(plan.config);
2239
+ await recordInstallTransaction([], [{ packageId: `mcp-recipe:${id}`, plan: plan.config }], snapshot.id);
2177
2240
  console.log(options.json
2178
2241
  ? JSON.stringify({ plan, snapshot }, null, 2)
2179
2242
  : `${formatMcpRecipePlan(plan)}\nConfigured. Snapshot: ${snapshot.id}\nAuthorize the service separately, then run: loadout mcp-recipe ${id} --config ${plan.config.path} --verify`);
@@ -2528,7 +2591,7 @@ program
2528
2591
  });
2529
2592
  console.log(formatPreparedCatalogInstall(prepared));
2530
2593
  if (!options.yes) {
2531
- console.log("Preview complete; nothing was changed. Use --yes after reviewing to install this exact plan.");
2594
+ console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
2532
2595
  return;
2533
2596
  }
2534
2597
  const snapshotId = await applyPreparedCatalogInstall(prepared, {
@@ -2608,30 +2671,112 @@ program
2608
2671
  const chosen = options.snapshot ?? snapshotIds.at(-1);
2609
2672
  if (!chosen)
2610
2673
  throw new Error("No Loadout snapshots found");
2611
- await restoreSnapshot(await readSnapshot(chosen));
2674
+ await restoreSnapshot(await readSnapshot(chosen), {
2675
+ requireUnchangedPostMutationState: true,
2676
+ });
2612
2677
  return chosen;
2613
2678
  });
2614
2679
  console.log(`Restored snapshot ${selected}`);
2615
2680
  });
2616
2681
  program
2617
2682
  .command("update")
2618
- .description("Plan updates, or apply one explicitly selected package update")
2683
+ .description("Check the saved profile and every managed package for updates")
2619
2684
  .option("--json", "emit machine-readable JSON")
2620
- .option("--apply", "apply the selected update")
2685
+ .option("--yes", "apply reviewed profile drift and safe screened package updates")
2686
+ .option("--apply", "legacy alias for --yes with --package")
2621
2687
  .option("--package <id>", "managed package id to update")
2622
2688
  .option("--approve-risk", "explicitly approve an update containing blocked safety findings")
2623
2689
  .action(async (options) => {
2624
- if (options.apply) {
2625
- if (!options.package)
2626
- throw new Error("--apply requires --package <id>");
2690
+ const applying = Boolean(options.apply || options.yes);
2691
+ if (options.apply && !options.package)
2692
+ throw new Error("--apply requires --package <id>");
2693
+ if (applying && options.package) {
2627
2694
  const result = await applyPackageUpdate(options.package, {
2628
2695
  approveRisk: options.approveRisk,
2629
2696
  });
2630
- console.log(`Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
2697
+ console.log(options.json
2698
+ ? JSON.stringify({ packageId: options.package, ...result }, null, 2)
2699
+ : `Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
2631
2700
  return;
2632
2701
  }
2633
- const plans = await buildUpdatePlan();
2634
- console.log(options.json ? JSON.stringify(plans, null, 2) : formatUpdatePlan(plans));
2702
+ const catalog = await loadEffectiveCatalog();
2703
+ let profile = await evaluateInstalledProfile(catalog);
2704
+ let profileSnapshotId;
2705
+ const appliedPackages = [];
2706
+ const failedPackages = [];
2707
+ if (applying && profile.installed && profile.needsRefresh) {
2708
+ const saved = (await readInstallState()).profile;
2709
+ const prepared = await prepareCatalogInstall({
2710
+ mode: saved.mode,
2711
+ ...(saved.packageIds ? { packageIds: saved.packageIds } : {}),
2712
+ }, {
2713
+ requestedAgents: saved.agents,
2714
+ catalog,
2715
+ access: { modelApis: [] },
2716
+ onProgress: printSetupProgress,
2717
+ });
2718
+ profileSnapshotId = await applyPreparedCatalogInstall(prepared, {
2719
+ approveRisk: options.approveRisk,
2720
+ });
2721
+ if (!options.json)
2722
+ console.log(`Applied reviewed ${saved.mode} profile changes. Snapshot: ${profileSnapshotId}`);
2723
+ profile = await evaluateInstalledProfile(catalog);
2724
+ }
2725
+ if (!options.json)
2726
+ console.error("Checking managed repositories (4 at a time, 30s per network operation)…");
2727
+ let plans = await buildUpdatePlan(undefined, {
2728
+ packageId: options.package,
2729
+ onProgress: options.json
2730
+ ? undefined
2731
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
2732
+ });
2733
+ if (options.package && plans.length === 0)
2734
+ throw new Error(`No Loadout-managed installation named '${options.package}' was found. Run \`loadout list\` to see tracked packages.`);
2735
+ if (applying) {
2736
+ const safe = selectSafeAutomaticUpdates(plans);
2737
+ for (const plan of safe)
2738
+ try {
2739
+ await applyPackageUpdate(plan.packageId);
2740
+ appliedPackages.push(plan.packageId);
2741
+ }
2742
+ catch (error) {
2743
+ failedPackages.push({
2744
+ packageId: plan.packageId,
2745
+ error: error instanceof Error ? error.message : String(error),
2746
+ });
2747
+ }
2748
+ plans = await buildUpdatePlan(undefined, {
2749
+ onProgress: options.json
2750
+ ? undefined
2751
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
2752
+ });
2753
+ if (!options.json) {
2754
+ console.log(appliedPackages.length
2755
+ ? `Updated safely: ${appliedPackages.join(", ")}`
2756
+ : "No safe active package update needed applying.");
2757
+ for (const item of failedPackages)
2758
+ console.log(`Could not update ${item.packageId}: ${item.error}`);
2759
+ const held = plans.filter((plan) => plan.status === "update-available" &&
2760
+ (plan.approvalRequired || plan.disabledAgents?.length));
2761
+ if (held.length)
2762
+ console.log(`Held for explicit review: ${held.map((item) => item.packageId).join(", ")}`);
2763
+ }
2764
+ }
2765
+ console.log(options.json
2766
+ ? JSON.stringify({
2767
+ profile,
2768
+ packages: plans,
2769
+ ...(applying
2770
+ ? {
2771
+ applied: {
2772
+ profileSnapshotId,
2773
+ packages: appliedPackages,
2774
+ failures: failedPackages,
2775
+ },
2776
+ }
2777
+ : {}),
2778
+ }, null, 2)
2779
+ : `${formatInstalledProfileStatus(profile)}\n\n${formatUpdatePlan(plans)}`);
2635
2780
  });
2636
2781
  program
2637
2782
  .command("convert")
@@ -2732,7 +2877,19 @@ program
2732
2877
  });
2733
2878
  await handle.close();
2734
2879
  });
2735
- program.action(() => runSetup({ package: [] }));
2880
+ for (const command of program.commands)
2881
+ // Commander supports `hidden` when a command is created. These commands are
2882
+ // registered by separate feature blocks, so use the same runtime flag here
2883
+ // rather than `Option#hideHelp`, which does not exist on Command objects.
2884
+ if (HIDDEN_FROM_FIRST_SCREEN.has(command.name()))
2885
+ command._hidden = true;
2886
+ program.addHelpText("after", "\nStart here: `loadout guide` shows the safe everyday path. `loadout advanced` lists the retained maintainer and integration commands.\n");
2887
+ program.argument("[unknown-command]");
2888
+ program.action((unknownCommand) => {
2889
+ if (unknownCommand)
2890
+ throw new Error(`Unknown command '${unknownCommand}'. Run 'loadout --help' to list available commands.`);
2891
+ printBeginnerGuide();
2892
+ });
2736
2893
  try {
2737
2894
  await recoverPendingTransactions();
2738
2895
  await program.parseAsync();
@@ -156,9 +156,13 @@ async function activeTreeFiles(targets) {
156
156
  export async function buildLibraryStateReport() {
157
157
  const state = await readInstallState();
158
158
  const records = allActivationRecords(state.installs, state.activations ?? []);
159
+ const repositories = Object.fromEntries(state.installs
160
+ .filter((install) => Boolean(install.repository))
161
+ .map((install) => [install.packageId, install.repository]));
159
162
  return {
160
163
  generatedAt: new Date().toISOString(),
161
164
  records,
165
+ repositories,
162
166
  counts: {
163
167
  packages: new Set(records.map((record) => record.packageId)).size,
164
168
  downloaded: records.filter((record) => record.cacheState === "downloaded")
@@ -357,17 +361,48 @@ export async function applyActivationChange(plan) {
357
361
  });
358
362
  return applied.snapshotId;
359
363
  }
364
+ function formatLibraryHeadline(report) {
365
+ return `Managed library: ${report.counts.packages} package(s), ${report.counts.active} active, ${report.counts.disabled} disabled, ${report.counts.downloaded} cached`;
366
+ }
367
+ function countByAgent(report, activationState) {
368
+ const counts = new Map();
369
+ for (const record of report.records)
370
+ if (record.activationState === activationState)
371
+ counts.set(record.agent, (counts.get(record.agent) ?? 0) + 1);
372
+ return [...counts]
373
+ .sort(([left], [right]) => left.localeCompare(right))
374
+ .map(([agent, count]) => `${agent} ${count}`)
375
+ .join(", ");
376
+ }
377
+ /** Concise default for people with a broad Maximum library. */
378
+ export function formatLibrarySummary(report) {
379
+ if (!report.records.length)
380
+ return "No Loadout-managed skill activations exist.";
381
+ return [
382
+ formatLibraryHeadline(report),
383
+ `Active now: ${countByAgent(report, "active") || "none"}`,
384
+ `Disabled library: ${countByAgent(report, "disabled") || "none"}`,
385
+ "Disabled library skills are reviewed copies, not extra agent context.",
386
+ ...(report.migrationPending
387
+ ? [
388
+ `Migration pending: ${report.migrationPending} active installation(s) will be cached on first disable.`,
389
+ ]
390
+ : []),
391
+ "Use `loadout library --all` for every skill, source package, and activation state.",
392
+ ].join("\n");
393
+ }
394
+ /** Full per-skill provenance report for audits and explicit investigation. */
360
395
  export function formatLibraryStateReport(report) {
361
396
  if (!report.records.length)
362
397
  return "No Loadout-managed skill activations exist.";
363
398
  return [
364
- `Managed library: ${report.counts.packages} package(s), ${report.counts.active} active, ${report.counts.disabled} disabled, ${report.counts.downloaded} cached`,
399
+ formatLibraryHeadline(report),
365
400
  ...(report.migrationPending
366
401
  ? [
367
402
  `Migration pending: ${report.migrationPending} active installation(s) will be cached on first disable.`,
368
403
  ]
369
404
  : []),
370
- ...report.records.map((record) => `${record.packageId}${record.unitId ? `/${record.unitId}` : ""} — ${record.agent} — cache:${record.cacheState} review:${record.reviewState} install:${record.installationState} activation:${record.activationState} — ${record.targets.length} target(s)`),
405
+ ...report.records.map((record) => `${record.packageId}${record.unitId ? `/${record.unitId}` : ""}${report.repositories[record.packageId] ? ` (${report.repositories[record.packageId]})` : ""} — ${record.agent} — cache:${record.cacheState} review:${record.reviewState} install:${record.installationState} activation:${record.activationState} — ${record.targets.length} target(s)`),
371
406
  ].join("\n");
372
407
  }
373
408
  export function formatActivationPlan(plan) {
@@ -140,6 +140,16 @@ export function adapterCapabilities(agent) {
140
140
  throw new Error(`No adapter capability declaration for '${agent}'`);
141
141
  return found;
142
142
  }
143
+ /**
144
+ * Return the display names of agents with an explicitly declared native skill
145
+ * directory. This is a filesystem-support boundary, not a claim about native
146
+ * application runtime behavior.
147
+ */
148
+ export function supportedAdapterNames(adapters = ADAPTER_CAPABILITIES) {
149
+ return adapters
150
+ .filter((adapter) => adapter.components.skill === "native")
151
+ .map((adapter) => adapter.displayName);
152
+ }
143
153
  /**
144
154
  * Produce a read-only native-skill plan for one agent. This is deliberately
145
155
  * narrower than package normalization: adapters may only target a directory