loadout-ai 0.5.8 → 0.6.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 (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/MASTER_PLAN.md +9 -9
  3. package/README.md +84 -121
  4. package/catalog/discovered.json +59800 -10065
  5. package/catalog/packages.json +4 -3
  6. package/dist/src/cli.js +26 -2979
  7. package/dist/src/commands/agents.js +168 -0
  8. package/dist/src/commands/catalog.js +708 -0
  9. package/dist/src/commands/health.js +98 -0
  10. package/dist/src/commands/inventory.js +417 -0
  11. package/dist/src/commands/lifecycle.js +411 -0
  12. package/dist/src/commands/mcp.js +424 -0
  13. package/dist/src/commands/setup.js +67 -0
  14. package/dist/src/commands/sharing.js +358 -0
  15. package/dist/src/commands/support.js +246 -0
  16. package/dist/src/core/active-limit.js +2 -0
  17. package/dist/src/core/active-policy.js +2 -1
  18. package/dist/src/core/catalog-install.js +47 -10
  19. package/dist/src/core/catalog.js +15 -96
  20. package/dist/src/core/cli-guide.js +43 -37
  21. package/dist/src/core/completion.js +2 -20
  22. package/dist/src/core/doctor.js +73 -24
  23. package/dist/src/core/github.js +3 -2
  24. package/dist/src/core/handoff.js +161 -0
  25. package/dist/src/core/health-score-evidence.js +2 -2
  26. package/dist/src/core/health.js +83 -0
  27. package/dist/src/core/install.js +8 -3
  28. package/dist/src/core/loadout-card.js +14 -2
  29. package/dist/src/core/review-queue.js +7 -2
  30. package/dist/src/core/route.js +248 -0
  31. package/dist/src/core/skill-inventory.js +3 -3
  32. package/dist/src/core/skills.js +6 -7
  33. package/dist/src/core/source.js +10 -3
  34. package/dist/src/core/upgrade.js +10 -9
  35. package/docs/ACTIVE_SET.md +1 -1
  36. package/docs/ACTIVE_SET_POLICY.md +1 -1
  37. package/docs/CATALOG.md +2 -2
  38. package/docs/DEMO_SCRIPT.md +5 -5
  39. package/docs/DISCOVERED.md +250 -253
  40. package/docs/RELEASE_REVIEW.md +28 -143
  41. package/docs/SUBMISSION_COPY.md +4 -4
  42. package/docs/UPSTREAM_LICENSE_DECISIONS.md +19 -12
  43. package/docs/USER_TEST_GUIDE.md +1 -1
  44. package/docs/evidence/readme-claims.json +2 -14
  45. package/package.json +4 -3
  46. package/dist/src/core/benchmark-campaign.js +0 -496
  47. package/dist/src/core/benchmark-evidence.js +0 -458
  48. package/dist/src/core/benchmark-fixtures.js +0 -1157
  49. package/dist/src/core/benchmark-runner.js +0 -301
  50. package/dist/src/core/benchmark-trust.js +0 -795
  51. package/dist/src/core/catalog-release.js +0 -287
  52. package/dist/src/core/compatibility-intelligence.js +0 -465
  53. package/dist/src/core/ecosystem-import.js +0 -1072
  54. package/dist/src/core/head-to-head.js +0 -437
  55. package/dist/src/core/intelligence-feed-build.js +0 -59
  56. package/dist/src/core/intelligence-feed.js +0 -353
  57. package/dist/src/core/registry-api.js +0 -97
  58. package/dist/src/core/release-claims.js +0 -175
  59. package/dist/src/core/signing.js +0 -115
@@ -0,0 +1,98 @@
1
+ import { parseAgentSelection } from "../core/paths.js";
2
+ import { buildUpdatePlan } from "../core/update.js";
3
+ import { buildHealthReport, formatHealthReport } from "../core/health.js";
4
+ import { buildFreshnessAlerts, formatFreshnessAlerts, ignoreFreshnessAlert, pinReplacement, readReplacementPins, unpinReplacement, } from "../core/freshness-alerts.js";
5
+ import { formatAgentHealthScore } from "../core/agent-health-score.js";
6
+ import { buildLocalAgentHealthScores } from "../core/health-score-evidence.js";
7
+ export function registerHealth(program) {
8
+ program
9
+ .command("health")
10
+ .description("Quickly check agents, installed packages, and local file drift")
11
+ .option("--json", "emit machine-readable JSON")
12
+ .option("--updates", "also perform live network update checks")
13
+ .option("--explain", "add deterministic score dimensions, evidence, uncertainty, and remediation")
14
+ .option("--agents <ids>", "limit explained scores to selected agent ids")
15
+ .action(async (options) => {
16
+ if (options.updates && !options.json)
17
+ console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
18
+ const report = await buildHealthReport({
19
+ updates: options.updates
20
+ ? () => buildUpdatePlan(undefined, {
21
+ onProgress: options.json
22
+ ? undefined
23
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
24
+ })
25
+ : undefined,
26
+ });
27
+ if (options.agents && !options.explain)
28
+ throw new Error("--agents requires --explain");
29
+ const selectedAgents = parseAgentSelection(options.agents);
30
+ const scores = options.explain
31
+ ? (await buildLocalAgentHealthScores()).filter((score) => !selectedAgents || selectedAgents.includes(score.agent))
32
+ : [];
33
+ console.log(options.json
34
+ ? JSON.stringify(options.explain ? { report, scores } : report, null, 2)
35
+ : [
36
+ formatHealthReport(report),
37
+ ...scores.map((score) => `\n${formatAgentHealthScore(score)}`),
38
+ ].join("\n"));
39
+ });
40
+ program
41
+ .command("alerts")
42
+ .description("Explain evidence-backed archive, staleness, reviewed-commit, and permission alerts")
43
+ .option("--updates", "perform live update safety checks")
44
+ .option("--all", "include ignored alerts")
45
+ .option("--json", "emit machine-readable JSON")
46
+ .action(async (options) => {
47
+ const alerts = await buildFreshnessAlerts({
48
+ checkUpdates: options.updates,
49
+ });
50
+ const selected = options.all
51
+ ? alerts
52
+ : alerts.filter((alert) => !alert.ignored);
53
+ console.log(options.json
54
+ ? JSON.stringify(selected, null, 2)
55
+ : formatFreshnessAlerts(selected));
56
+ });
57
+ program
58
+ .command("alert-ignore")
59
+ .description("Ignore one exact freshness alert id on this machine")
60
+ .argument("<id>", "alert id shown by loadout alerts")
61
+ .action(async (id) => {
62
+ await ignoreFreshnessAlert(id);
63
+ console.log(`Ignored ${id} locally. Re-run loadout alerts --all to inspect it.`);
64
+ });
65
+ program
66
+ .command("alert-pin")
67
+ .description("Pin a reviewed replacement preference after comparing evidence; does not change active skills")
68
+ .argument("<package>", "currently installed package id")
69
+ .argument("<replacement>", "reviewed replacement package id")
70
+ .action(async (packageId, replacementId) => {
71
+ await pinReplacement(packageId, replacementId);
72
+ console.log(`Pinned ${replacementId} as a local replacement preference for ${packageId}. Review and activate it explicitly with loadout compare/enable.`);
73
+ });
74
+ program
75
+ .command("alert-unpin")
76
+ .description("Remove a local replacement preference")
77
+ .argument("<package>", "currently installed package id")
78
+ .action(async (packageId) => {
79
+ const removed = await unpinReplacement(packageId);
80
+ console.log(removed
81
+ ? `Removed the replacement preference for ${packageId}.`
82
+ : `No replacement preference exists for ${packageId}.`);
83
+ });
84
+ program
85
+ .command("alert-pins")
86
+ .description("Show local replacement preferences")
87
+ .option("--json", "emit machine-readable JSON")
88
+ .action(async (options) => {
89
+ const pins = await readReplacementPins();
90
+ console.log(options.json
91
+ ? JSON.stringify(pins, null, 2)
92
+ : pins.length
93
+ ? pins
94
+ .map((pin) => `${pin.packageId} -> ${pin.replacementPackageId}`)
95
+ .join("\n")
96
+ : "No local replacement preferences.");
97
+ });
98
+ }
@@ -0,0 +1,417 @@
1
+ import { loadEffectiveCatalog } from "../core/catalog.js";
2
+ import { detectAgents, parseAgentSelection } from "../core/paths.js";
3
+ import { resolve } from "node:path";
4
+ import { runDoctor, formatDoctorReport } from "../core/doctor.js";
5
+ import { applyProfileToManifest } from "../core/manifest.js";
6
+ import { buildHealthReport, formatStatusScreen, gradeHealth, } from "../core/health.js";
7
+ import { applyRemove, planRemove } from "../core/remove.js";
8
+ import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "../core/uninstall.js";
9
+ import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "../core/recommend.js";
10
+ import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "../core/improve.js";
11
+ import { applySyncPlan, buildSyncPlan } from "../core/sync.js";
12
+ import { ADAPTER_CAPABILITIES, buildAdapterCapabilityGaps, formatAdapterCapabilityGaps, formatCapabilityMatrix, } from "../core/adapters.js";
13
+ import { formatAgentInventory, inspectAgents, } from "../core/agent-inspection.js";
14
+ import { formatInstalledSkillInventory, scanInstalledSkills, } from "../core/skill-inventory.js";
15
+ import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "../core/provenance.js";
16
+ import { compareSkill, formatSkillComparison } from "../core/skill-compare.js";
17
+ import { applySkillAdoption, formatAdoptionPlan, planSkillAdoption, } from "../core/adopt.js";
18
+ import { applyReconcilePlan, buildReconcilePlan, formatReconcilePlan, } from "../core/reconcile.js";
19
+ import { readLocalOutcomes } from "../core/outcomes.js";
20
+ import { formatAgentVersions, inspectAgentVersions, } from "../core/agent-versions.js";
21
+ import { scanSkillSecurity } from "../core/skill-security.js";
22
+ import { printProvenanceProgress } from "./support.js";
23
+ export function registerInventory(program) {
24
+ program
25
+ .command("remove")
26
+ .description("Safely remove only files managed for one package")
27
+ .argument("<package>", "managed package id")
28
+ .option("--yes", "apply removal; otherwise show a plan")
29
+ .option("--force", "also remove managed files changed outside Loadout")
30
+ .action(async (packageId, options) => {
31
+ const plan = await planRemove(packageId);
32
+ console.log(JSON.stringify(plan, null, 2));
33
+ if (!options.yes)
34
+ return console.log("Dry run only. Re-run with --yes to apply this removal plan.");
35
+ const snapshot = await applyRemove(plan, { force: options.force });
36
+ console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
37
+ });
38
+ program
39
+ .command("uninstall")
40
+ .description("Preview or completely remove Loadout-managed data")
41
+ .option("--yes", "remove managed data; otherwise show a preview")
42
+ .option("--force", "also remove managed files changed outside Loadout")
43
+ .option("--remove-cli", "also uninstall the global loadout-ai npm command")
44
+ .action(async (options) => {
45
+ const plan = await buildUninstallPlan();
46
+ console.log(formatUninstallPlan(plan, { applying: Boolean(options.yes) }));
47
+ if (!options.yes)
48
+ return;
49
+ const result = await applyUninstall(plan, undefined, {
50
+ force: options.force,
51
+ onProgress: (message) => console.log(`→ ${message}`),
52
+ });
53
+ console.log(`Removed ${result.removedPackages} managed package(s), ${result.removedRuntimeTools} runtime tool(s), daily jobs, cache, library, and Loadout state.`);
54
+ if (options.removeCli) {
55
+ await uninstallGlobalCli();
56
+ console.log("Removed the global loadout-ai CLI. Open a new shell before checking the command again.");
57
+ }
58
+ else {
59
+ console.log("The CLI is still installed. Remove it too with: npm uninstall -g loadout-ai");
60
+ }
61
+ });
62
+ program
63
+ .command("recommend")
64
+ .description("Recommend catalog packages from local project signals")
65
+ .option("--project <path>", "project directory", process.cwd())
66
+ .option("--agent <id>", "personalize with local-only outcomes recorded for one supported agent")
67
+ .option("--json", "emit machine-readable JSON")
68
+ .action(async (options) => {
69
+ const signals = await scanProject(options.project);
70
+ let recommendations = recommendPackages(signals, await loadEffectiveCatalog());
71
+ if (options.agent) {
72
+ const agents = parseAgentSelection(options.agent);
73
+ if (agents.length !== 1)
74
+ throw new Error("--agent accepts exactly one id");
75
+ recommendations = personalizeRecommendations(recommendations, signals, await readLocalOutcomes(), agents[0]);
76
+ }
77
+ console.log(options.json
78
+ ? JSON.stringify({
79
+ signals,
80
+ recommendations,
81
+ recommendationBoundary: RECOMMENDATION_BOUNDARY,
82
+ personalization: options.agent
83
+ ? {
84
+ agent: options.agent,
85
+ privacy: "local-only-no-project-or-content",
86
+ }
87
+ : undefined,
88
+ }, null, 2)
89
+ : formatRecommendations(signals, recommendations));
90
+ });
91
+ program
92
+ .command("profiles")
93
+ .description("List Loadout policy profiles or inspect one profile")
94
+ .argument("[name]", "profile name")
95
+ .option("--json", "emit machine-readable JSON")
96
+ .option("--apply-to <path>", "add the selected profile packages to a manifest")
97
+ .action(async (name, options) => {
98
+ if (!name)
99
+ return console.log(options.json
100
+ ? JSON.stringify(TESTED_PROFILES, null, 2)
101
+ : Object.entries(TESTED_PROFILES)
102
+ .map(([id, profile]) => `${id} — ${profile.description}`)
103
+ .join("\n"));
104
+ if (name === "custom" && !options.applyTo) {
105
+ const profile = TESTED_PROFILES.custom;
106
+ return console.log(options.json
107
+ ? JSON.stringify({ name, ...profile, packages: [] }, null, 2)
108
+ : `custom: ${profile.description}\n Use: loadout setup --mode custom --package <id>`);
109
+ }
110
+ const packages = profileManifestPackages(name, await loadEffectiveCatalog());
111
+ if (options.applyTo) {
112
+ const manifest = await applyProfileToManifest(options.applyTo, name, packages);
113
+ console.log(`Applied profile ${name} to ${options.applyTo}. ${manifest.packages.length} package(s) configured.`);
114
+ return;
115
+ }
116
+ console.log(options.json
117
+ ? JSON.stringify({ name, ...TESTED_PROFILES[name], packages }, null, 2)
118
+ : `${name}: ${TESTED_PROFILES[name].description}\n${packages.map((pkg) => ` ${pkg.id} — ${pkg.repository}`).join("\n")}`);
119
+ });
120
+ program
121
+ .command("improve")
122
+ .description("Propose the next evidence-backed improvement without changing anything")
123
+ .option("--json", "emit machine-readable JSON")
124
+ .option("--write", "persist the cycle record and reusable prompt locally")
125
+ .option("--output <directory>", "output directory; defaults to private Loadout state")
126
+ .action(async (options) => {
127
+ const cycle = await buildImprovementCycle();
128
+ console.log(options.json
129
+ ? JSON.stringify(cycle, null, 2)
130
+ : formatImprovementCycle(cycle));
131
+ if (options.write) {
132
+ const paths = await writeImprovementCycle(cycle, options.output);
133
+ console.log(`Cycle record: ${paths.json}\nLoop prompt: ${paths.prompt}`);
134
+ }
135
+ });
136
+ program
137
+ .command("improve-feedback")
138
+ .description("Record a human-reviewed outcome for a persisted improvement cycle")
139
+ .requiredOption("--id <id>", "cycle id")
140
+ .requiredOption("--outcome <outcome>", "success, failure, or partial")
141
+ .option("--note <text>", "short non-secret lesson")
142
+ .option("--directory <path>", "improvement history directory")
143
+ .action(async (options) => {
144
+ if (!["success", "failure", "partial"].includes(options.outcome))
145
+ throw new Error("--outcome must be success, failure, or partial");
146
+ await recordImprovementOutcome(options.id, options.outcome, options.note, options.directory);
147
+ console.log(`Recorded ${options.outcome} outcome for ${options.id}.`);
148
+ });
149
+ program
150
+ .command("sync")
151
+ .description("Reproduce a loadout.json manifest as one safe transaction")
152
+ .option("--manifest <path>", "manifest path", "loadout.json")
153
+ .option("--lock <path>", "lockfile output", "loadout.lock")
154
+ .option("--yes", "apply the plan; otherwise remain read-only")
155
+ .option("--approve-risk", "explicitly approve plans containing scripts, hooks, or binaries")
156
+ .action(async (options) => {
157
+ const plan = await buildSyncPlan(options.manifest);
158
+ console.log(JSON.stringify({
159
+ manifest: plan.manifest,
160
+ packages: plan.packages.map((entry) => ({
161
+ ...entry.plan,
162
+ safety: entry.safety,
163
+ })),
164
+ mcpChanges: plan.mcpPlans.map((entry) => ({
165
+ packageId: entry.packageId,
166
+ path: entry.plan.path,
167
+ changes: entry.plan.changes,
168
+ warnings: entry.plan.warnings,
169
+ })),
170
+ skipped: plan.skipped,
171
+ policyViolations: plan.policyViolations,
172
+ }, null, 2));
173
+ if (!options.yes)
174
+ return console.log("Dry run only. Re-run with --yes to synchronize this Loadout.");
175
+ const result = await applySyncPlan(plan, options.lock, {
176
+ approveRisk: options.approveRisk,
177
+ });
178
+ console.log(`Synchronized successfully.${result.snapshotId ? ` Snapshot: ${result.snapshotId}.` : ""} Lockfile: ${result.lockfile}`);
179
+ });
180
+ program
181
+ .command("scan")
182
+ .description("Inventory existing agent skills, ownership, fingerprints, and duplicates without changing anything")
183
+ .option("--agents <ids>", "comma-separated agent ids; defaults to detected agents")
184
+ .option("--refresh-provenance", "fetch exact reviewed commits and rebuild the local catalog skill index")
185
+ .option("--json", "emit the complete machine-readable inventory")
186
+ .action(async (options) => {
187
+ const detected = await detectAgents();
188
+ const requested = parseAgentSelection(options.agents);
189
+ const selected = requested?.length
190
+ ? detected.filter((agent) => requested.includes(agent.id))
191
+ : detected.filter((agent) => agent.installed);
192
+ if (!selected.length)
193
+ throw new Error("No detected agent profile is available to scan");
194
+ const report = await scanInstalledSkills(selected);
195
+ const catalog = await loadEffectiveCatalog();
196
+ const resolved = await resolveCatalogSkillIndex({
197
+ refresh: options.refreshProvenance,
198
+ offline: !options.refreshProvenance,
199
+ build: {
200
+ catalog,
201
+ onProgress: options.refreshProvenance
202
+ ? printProvenanceProgress
203
+ : undefined,
204
+ },
205
+ });
206
+ const enriched = enrichInventoryWithProvenance(report, resolved.index, resolved.source);
207
+ console.log(options.json
208
+ ? JSON.stringify(enriched, null, 2)
209
+ : `${formatInstalledSkillInventory(enriched)}\n${formatProvenanceSummary(enriched)}`);
210
+ });
211
+ program
212
+ .command("reconcile")
213
+ .description("Match existing unmanaged skills to pinned sources, group mirrors, and preview safe ownership or updates")
214
+ .option("--agents <ids>", "comma-separated agent ids; defaults to detected agents")
215
+ .option("--refresh", "rebuild the inspected catalog skill index")
216
+ .option("--replace-outdated", "replace unambiguous outdated trees with their reviewed pinned source")
217
+ .option("--approve-risk", "approve the exact safety findings shown for outdated replacements")
218
+ .option("--yes", "apply the displayed transaction; otherwise preview")
219
+ .option("--json", "emit machine-readable output")
220
+ .action(async (options) => {
221
+ const detected = await detectAgents();
222
+ const requested = parseAgentSelection(options.agents);
223
+ const selected = requested?.length
224
+ ? detected.filter((agent) => requested.includes(agent.id) && agent.installed)
225
+ : detected.filter((agent) => agent.installed);
226
+ if (!selected.length)
227
+ throw new Error("No detected agent profile is available to reconcile");
228
+ const resolved = await resolveCatalogSkillIndex({
229
+ refresh: options.refresh,
230
+ build: {
231
+ catalog: await loadEffectiveCatalog(),
232
+ onProgress: options.refresh ? printProvenanceProgress : undefined,
233
+ },
234
+ });
235
+ if (!resolved.index)
236
+ throw new Error("No reviewed catalog skill index is available");
237
+ const plan = await buildReconcilePlan(selected, resolved.index);
238
+ if (!options.yes) {
239
+ console.log(options.json
240
+ ? JSON.stringify(plan, null, 2)
241
+ : `${formatReconcilePlan(plan)}\n\nDry run only. Re-run with --yes to adopt exact matches.${plan.summary.outdated ? " Add --replace-outdated to include the reviewed updates shown above." : ""}`);
242
+ return;
243
+ }
244
+ const result = await applyReconcilePlan(plan, {
245
+ replaceOutdated: options.replaceOutdated,
246
+ approveRisk: options.approveRisk,
247
+ });
248
+ console.log(options.json
249
+ ? JSON.stringify({ plan, result }, null, 2)
250
+ : `${formatReconcilePlan(plan)}\n\nReconciled ${result.adopted} exact group(s) and ${result.updated} outdated group(s). Snapshot: ${result.snapshotId}`);
251
+ });
252
+ program
253
+ .command("adopt")
254
+ .description("Take ownership of one explicitly selected installed skill without changing its bytes")
255
+ .argument("<skill>", "installed skill name, directory name, or exact path")
256
+ .requiredOption("--agent <id>", "agent that owns the installed skill")
257
+ .option("--refresh-provenance", "rebuild the inspected catalog skill index")
258
+ .option("--yes", "record ownership; otherwise show a dry-run plan")
259
+ .option("--json", "emit machine-readable JSON")
260
+ .action(async (skill, options) => {
261
+ const detected = await detectAgents();
262
+ const agent = detected.find((item) => item.id === options.agent && item.installed);
263
+ if (!agent)
264
+ throw new Error(`Agent '${options.agent}' is unknown or is not installed`);
265
+ const resolved = await resolveCatalogSkillIndex({
266
+ refresh: options.refreshProvenance,
267
+ offline: !options.refreshProvenance,
268
+ build: {
269
+ catalog: await loadEffectiveCatalog(),
270
+ onProgress: options.refreshProvenance
271
+ ? printProvenanceProgress
272
+ : undefined,
273
+ },
274
+ });
275
+ const plan = await planSkillAdoption(skill, agent, resolved.index);
276
+ if (!options.yes) {
277
+ console.log(options.json
278
+ ? JSON.stringify(plan, null, 2)
279
+ : `${formatAdoptionPlan(plan)}\nDry run only. Re-run with --yes to adopt this one skill.`);
280
+ return;
281
+ }
282
+ const snapshotId = await applySkillAdoption(plan);
283
+ console.log(options.json
284
+ ? JSON.stringify({ plan, snapshotId }, null, 2)
285
+ : `${formatAdoptionPlan(plan)}\nAdopted without changing skill bytes. Snapshot: ${snapshotId}`);
286
+ });
287
+ program
288
+ .command("compare")
289
+ .description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
290
+ .argument("<skill>", "installed skill name, directory name, or catalog package id")
291
+ .option("--agent <id>", "select one installed agent when names are ambiguous")
292
+ .option("--refresh", "rebuild the inspected catalog skill index")
293
+ .option("--offline", "never fetch; use the existing local index only")
294
+ .option("--limit <count>", "maximum alternatives to show", "10")
295
+ .option("--json", "emit the complete machine-readable comparison")
296
+ .action(async (skill, options) => {
297
+ if (options.refresh && options.offline)
298
+ throw new Error("--refresh and --offline cannot be used together");
299
+ const limit = Number(options.limit);
300
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
301
+ throw new Error("--limit must be an integer from 1 to 50");
302
+ const detected = await detectAgents();
303
+ const agent = options.agent;
304
+ if (agent && !detected.some((item) => item.id === agent))
305
+ throw new Error(`Unknown agent id: ${options.agent}`);
306
+ const selected = detected.filter((item) => item.installed);
307
+ if (!selected.length)
308
+ throw new Error("No detected agent profile is available to compare");
309
+ const catalog = await loadEffectiveCatalog();
310
+ const resolved = await resolveCatalogSkillIndex({
311
+ refresh: options.refresh,
312
+ offline: options.offline,
313
+ build: {
314
+ catalog,
315
+ onProgress: options.offline ? undefined : printProvenanceProgress,
316
+ },
317
+ });
318
+ if (!resolved.index)
319
+ throw new Error("No local catalog skill index exists. Re-run without --offline or use --refresh.");
320
+ const inventory = enrichInventoryWithProvenance(await scanInstalledSkills(selected), resolved.index, resolved.source);
321
+ const comparison = compareSkill(skill, inventory, resolved.index.records, catalog, {
322
+ ...(agent ? { agent } : {}),
323
+ limit,
324
+ indexGeneratedAt: resolved.index.generatedAt,
325
+ failures: resolved.index.failures,
326
+ });
327
+ console.log(options.json
328
+ ? JSON.stringify(comparison, null, 2)
329
+ : formatSkillComparison(comparison));
330
+ });
331
+ program
332
+ .command("status")
333
+ .description("Home screen: a health grade, detected agents, and the next thing to do")
334
+ .option("--json", "emit machine-readable status")
335
+ .action(async (options) => {
336
+ const agents = await detectAgents();
337
+ const [inventory, report] = await Promise.all([
338
+ inspectAgents(agents),
339
+ buildHealthReport({}),
340
+ ]);
341
+ if (options.json)
342
+ return console.log(JSON.stringify({ grade: gradeHealth(report), report, inventory }, null, 2));
343
+ console.log(formatStatusScreen(report, inventory.map((item) => formatAgentInventory(item))));
344
+ });
345
+ program
346
+ .command("versions")
347
+ .description("Detect installed agent versions with bounded read-only commands")
348
+ .option("--json", "emit machine-readable version evidence")
349
+ .action(async (options) => {
350
+ const evidence = await inspectAgentVersions();
351
+ console.log(options.json
352
+ ? JSON.stringify(evidence, null, 2)
353
+ : formatAgentVersions(evidence));
354
+ });
355
+ program
356
+ .command("skill-audit")
357
+ .description("Statically inspect one Agent Skill and emit its security/capability inventory")
358
+ .argument("<directory>", "skill directory containing SKILL.md")
359
+ .option("--json", "emit the complete machine-readable report")
360
+ .action(async (directory, options) => {
361
+ const report = await scanSkillSecurity(resolve(directory));
362
+ console.log(options.json
363
+ ? JSON.stringify(report, null, 2)
364
+ : [
365
+ `Skill audit: ${report.specification.name ?? report.rootName}`,
366
+ `Verdict: ${report.verdict}`,
367
+ `Inventory: ${report.inventory.totalFiles} files, ${report.inventory.totalBytes} bytes, sha256:${report.inventory.treeHash}`,
368
+ `Capabilities: ${report.capabilities.executableFiles.length} executable/script files, ${report.capabilities.dependencyNames.length} named dependencies, ${report.capabilities.domains.length} domains, ${report.capabilities.environmentNames.length} environment names`,
369
+ ...report.deterministicFindings.map((item) => `${item.severity === "critical" ? "✗" : "!"} ${item.severity}/${item.category}: ${item.message} (${item.paths.join(", ")})`),
370
+ ...(!report.deterministicFindings.length
371
+ ? [
372
+ "No deterministic findings within the bounded static policy.",
373
+ ]
374
+ : []),
375
+ "Static inspection cannot prove runtime safety or task quality.",
376
+ ].join("\n"));
377
+ if (report.verdict === "blocked")
378
+ process.exitCode = 2;
379
+ });
380
+ program
381
+ .command("capabilities")
382
+ .description("Show honest native, adapted, and unsupported adapter capabilities")
383
+ .option("--json", "emit machine-readable JSON")
384
+ .option("--inspect", "also inspect managed component directories on this machine")
385
+ .option("--gaps", "show the evidence-gated backlog for unsupported adapter combinations")
386
+ .action(async (options) => {
387
+ if (options.gaps) {
388
+ if (options.inspect)
389
+ throw new Error("Choose either --inspect or --gaps");
390
+ const gaps = buildAdapterCapabilityGaps();
391
+ return console.log(options.json
392
+ ? JSON.stringify(gaps, null, 2)
393
+ : formatAdapterCapabilityGaps());
394
+ }
395
+ if (!options.inspect)
396
+ return console.log(options.json
397
+ ? JSON.stringify(ADAPTER_CAPABILITIES, null, 2)
398
+ : formatCapabilityMatrix());
399
+ const inventory = await inspectAgents(await detectAgents());
400
+ if (options.json)
401
+ return console.log(JSON.stringify({ capabilities: ADAPTER_CAPABILITIES, inventory }, null, 2));
402
+ console.log(`${formatCapabilityMatrix()}\n\nLocal managed-component inventory:`);
403
+ for (const item of inventory)
404
+ console.log(formatAgentInventory(item));
405
+ });
406
+ program
407
+ .command("doctor")
408
+ .description("Check agents, skill directories, permissions, and Loadout setup")
409
+ .option("--verbose", "show per-component detail for each agent")
410
+ .option("--json", "print a machine-readable report")
411
+ .action(async (options) => {
412
+ const report = await runDoctor();
413
+ console.log(options.json
414
+ ? JSON.stringify(report, null, 2)
415
+ : formatDoctorReport(report, { verbose: options.verbose }));
416
+ });
417
+ }