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,411 @@
1
+ import { loadEffectiveCatalog, } from "../core/catalog.js";
2
+ import { detectAgents, parseAgentSelection } from "../core/paths.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { buildSkillPlan, applySkillInstall, installedAgents, } from "../core/install.js";
6
+ import { listSnapshotIds, readSnapshot, restoreSnapshot, summarizeSnapshot, } from "../core/snapshot.js";
7
+ import { fetchRepositorySnapshot } from "../core/source.js";
8
+ import { applyPackageUpdate, buildUpdatePlan, formatUpdatePlan, selectSafeAutomaticUpdates, } from "../core/update.js";
9
+ import { startApiServer } from "../core/api.js";
10
+ import { readInstallState } from "../core/state.js";
11
+ import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "../core/profile-state.js";
12
+ import { resolveCatalogProfile } from "../core/profiles.js";
13
+ import { compileConversion, } from "../core/conversion.js";
14
+ import { writeFileAtomically } from "../core/atomic-file.js";
15
+ import { formatCanaryResult, runCanary } from "../core/canary.js";
16
+ import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "../core/catalog-install.js";
17
+ import { withMutationLock } from "../core/transaction.js";
18
+ import { setupSelection, printSetupProgress } from "./support.js";
19
+ export function registerLifecycle(program) {
20
+ program
21
+ .command("plan")
22
+ .description("Plan installing packages from a local directory, catalog, or public GitHub repository")
23
+ .option("--source <directory>", "local package directory containing SKILL.md")
24
+ .option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
25
+ .option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
26
+ .option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
27
+ .option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
28
+ .action(async (options) => {
29
+ const packageIds = options.package ?? [];
30
+ const hasSource = Boolean(options.source || options.repository);
31
+ if (hasSource && options.mode)
32
+ throw new Error("--mode cannot be combined with --source or --repository");
33
+ if (hasSource && packageIds.length !== 1)
34
+ throw new Error("A source or repository requires exactly one --package");
35
+ if (!hasSource && !options.mode)
36
+ throw new Error("Provide --mode or exactly one of --source/--repository");
37
+ if (!hasSource) {
38
+ const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
39
+ requestedAgents: parseAgentSelection(options.agents),
40
+ onProgress: printSetupProgress,
41
+ });
42
+ console.log(JSON.stringify({
43
+ mode: prepared.selection.mode,
44
+ agents: prepared.agents.map((agent) => agent.id),
45
+ packages: prepared.entries.map((entry) => ({
46
+ ...entry.plan,
47
+ repository: entry.metadata?.repository,
48
+ resolvedCommit: entry.metadata?.resolvedCommit,
49
+ safety: entry.safety,
50
+ })),
51
+ skipped: prepared.skipped,
52
+ profile: {
53
+ deferred: prepared.resolution.deferred.map((pkg) => pkg.id),
54
+ conflicts: prepared.resolution.conflicts,
55
+ warnings: prepared.resolution.warnings,
56
+ },
57
+ }, null, 2));
58
+ return;
59
+ }
60
+ const resolution = hasSource
61
+ ? undefined
62
+ : resolveCatalogProfile(await loadEffectiveCatalog(), {
63
+ mode: options.mode,
64
+ packageIds,
65
+ });
66
+ const selected = hasSource
67
+ ? [{ id: packageIds[0] }]
68
+ : resolution.packages;
69
+ const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
70
+ const plans = [];
71
+ const skipped = [];
72
+ for (const pkg of selected) {
73
+ const fetched = options.repository
74
+ ? await fetchRepositorySnapshot(options.repository)
75
+ : !options.source
76
+ ? await fetchRepositorySnapshot(pkg.repository)
77
+ : undefined;
78
+ try {
79
+ plans.push(await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents));
80
+ }
81
+ catch (error) {
82
+ if (!options.mode ||
83
+ !(error instanceof Error) ||
84
+ !error.message.startsWith("No SKILL.md found"))
85
+ throw error;
86
+ skipped.push({
87
+ packageId: pkg.id,
88
+ reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
89
+ });
90
+ }
91
+ }
92
+ console.log(JSON.stringify(options.mode
93
+ ? {
94
+ mode: options.mode,
95
+ packages: plans,
96
+ skipped,
97
+ profile: resolution && {
98
+ deferred: resolution.deferred.map((pkg) => pkg.id),
99
+ conflicts: resolution.conflicts,
100
+ warnings: resolution.warnings,
101
+ },
102
+ }
103
+ : plans[0], null, 2));
104
+ });
105
+ program
106
+ .command("install")
107
+ .description("Install packages from a local directory, catalog, or public GitHub repository")
108
+ .option("--source <directory>", "local package directory containing SKILL.md")
109
+ .option("--repository <owner/repo>", "public GitHub repository containing SKILL.md")
110
+ .option("--package <id>", "package identifier (repeat for custom mode)", (value, previous = []) => [...previous, value], [])
111
+ .option("--mode <mode>", "catalog selection mode: stable, power, maximum, or custom")
112
+ .option("--agents <ids>", "comma-separated agent ids; defaults to all detected agents")
113
+ .option("--yes", "apply without interactive confirmation")
114
+ .option("--approve-risk", "approve reviewed safety findings for catalog mode")
115
+ .option("--details", "show every quarantined and deferred unit")
116
+ .action(async (options) => {
117
+ const packageIds = options.package ?? [];
118
+ const hasSource = Boolean(options.source || options.repository);
119
+ if (hasSource && options.mode)
120
+ throw new Error("--mode cannot be combined with --source or --repository");
121
+ if (hasSource && packageIds.length !== 1)
122
+ throw new Error("A source or repository requires exactly one --package");
123
+ if (!hasSource && !options.mode)
124
+ throw new Error("Provide --mode or exactly one of --source/--repository");
125
+ if (!hasSource) {
126
+ const prepared = await prepareCatalogInstall(setupSelection(options.mode, packageIds), {
127
+ requestedAgents: parseAgentSelection(options.agents),
128
+ onProgress: printSetupProgress,
129
+ additive: options.mode === "custom",
130
+ });
131
+ console.log(formatPreparedCatalogInstall(prepared, {
132
+ details: options.details,
133
+ }));
134
+ if (!options.yes) {
135
+ console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
136
+ return;
137
+ }
138
+ const snapshotId = await applyPreparedCatalogInstall(prepared, {
139
+ approveRisk: options.approveRisk,
140
+ });
141
+ console.log(`Installed ${prepared.entries.length} repositories as one transaction. Snapshot: ${snapshotId}`);
142
+ return;
143
+ }
144
+ const resolution = hasSource
145
+ ? undefined
146
+ : resolveCatalogProfile(await loadEffectiveCatalog(), {
147
+ mode: options.mode,
148
+ packageIds,
149
+ });
150
+ const selected = hasSource
151
+ ? [{ id: packageIds[0] }]
152
+ : resolution.packages;
153
+ const agents = installedAgents(await detectAgents(), parseAgentSelection(options.agents));
154
+ const plans = [];
155
+ const skipped = [];
156
+ for (const pkg of selected) {
157
+ const fetched = options.repository
158
+ ? await fetchRepositorySnapshot(options.repository)
159
+ : !options.source
160
+ ? await fetchRepositorySnapshot(pkg.repository)
161
+ : undefined;
162
+ try {
163
+ plans.push({
164
+ plan: await buildSkillPlan(fetched?.path ?? options.source, pkg.id, agents),
165
+ repository: fetched?.repository,
166
+ commit: fetched?.commit,
167
+ });
168
+ }
169
+ catch (error) {
170
+ if (!options.mode ||
171
+ !(error instanceof Error) ||
172
+ !error.message.startsWith("No SKILL.md found"))
173
+ throw error;
174
+ skipped.push({
175
+ packageId: pkg.id,
176
+ reason: "No SKILL.md found; this package is not skill-installable yet (inspect its MCP manifest instead).",
177
+ });
178
+ }
179
+ }
180
+ console.log(`Installing ${plans.map(({ plan }) => plan.packageId).join(", ")} for ${agents.map((agent) => agent.id).join(", ")}...`);
181
+ for (const warning of resolution?.warnings ?? [])
182
+ console.log(`Profile warning: ${warning}`);
183
+ for (const entry of skipped)
184
+ console.log(`Skipping ${entry.packageId}: ${entry.reason}`);
185
+ if (!options.yes)
186
+ console.log("Review the plan with `loadout plan`; use --yes to apply it.");
187
+ if (!options.yes)
188
+ return;
189
+ for (const entry of plans) {
190
+ const snapshotId = await applySkillInstall(entry.plan, entry.repository
191
+ ? { repository: entry.repository, resolvedCommit: entry.commit }
192
+ : undefined);
193
+ console.log(`Installed ${entry.plan.packageId} successfully. Snapshot: ${snapshotId}`);
194
+ }
195
+ });
196
+ program
197
+ .command("rollback")
198
+ .description("Restore the most recent Loadout snapshot")
199
+ .option("--snapshot <id>", "specific snapshot id")
200
+ .option("--list", "list snapshot ids without restoring anything")
201
+ .action(async (options) => {
202
+ if (options.list) {
203
+ const snapshotIds = await listSnapshotIds();
204
+ if (!snapshotIds.length)
205
+ return console.log("No Loadout snapshots found.");
206
+ console.log("Loadout rollback history (oldest to newest):");
207
+ for (const [index, id] of snapshotIds.entries()) {
208
+ const summary = summarizeSnapshot(await readSnapshot(id));
209
+ const impact = summary.changedEntries === undefined
210
+ ? "legacy impact unknown"
211
+ : `${summary.changedEntries} changed filesystem entr${summary.changedEntries === 1 ? "y" : "ies"}`;
212
+ console.log(`${index === snapshotIds.length - 1 ? "latest" : " "} ${summary.createdAt} ${summary.label} ${impact} ${summary.roots} root(s) ${id}`);
213
+ }
214
+ console.log("Restore an exact entry with: loadout rollback --snapshot <id>");
215
+ return;
216
+ }
217
+ const selected = await withMutationLock(async () => {
218
+ const snapshotIds = await listSnapshotIds();
219
+ const chosen = options.snapshot ?? snapshotIds.at(-1);
220
+ if (!chosen)
221
+ throw new Error("No Loadout snapshots found");
222
+ const snapshot = await readSnapshot(chosen);
223
+ await restoreSnapshot(snapshot, {
224
+ requireUnchangedPostMutationState: true,
225
+ });
226
+ return summarizeSnapshot(snapshot);
227
+ });
228
+ console.log(`Restored ${selected.label} snapshot ${selected.id} (${selected.changedEntries ?? "unknown"} recorded changed filesystem entries across ${selected.roots} root(s)).`);
229
+ if (selected.changedEntries === 0)
230
+ console.log("This snapshot recorded no effective filesystem change. Choose an older explicit snapshot if you meant to undo an earlier installation.");
231
+ });
232
+ program
233
+ .command("update")
234
+ .description("Check the saved profile and every managed package for updates")
235
+ .option("--json", "emit machine-readable JSON")
236
+ .option("--yes", "apply reviewed profile drift and safe screened package updates")
237
+ .option("--apply", "legacy alias for --yes with --package")
238
+ .option("--package <id>", "managed package id to update")
239
+ .option("--approve-risk", "explicitly approve an update containing blocked safety findings")
240
+ .action(async (options) => {
241
+ const applying = Boolean(options.apply || options.yes);
242
+ if (options.apply && !options.package)
243
+ throw new Error("--apply requires --package <id>");
244
+ if (applying && options.package) {
245
+ const result = await applyPackageUpdate(options.package, {
246
+ approveRisk: options.approveRisk,
247
+ });
248
+ console.log(options.json
249
+ ? JSON.stringify({ packageId: options.package, ...result }, null, 2)
250
+ : `Updated ${options.package} to ${result.commit}. Snapshot: ${result.snapshotId}`);
251
+ return;
252
+ }
253
+ const catalog = await loadEffectiveCatalog();
254
+ let profile = await evaluateInstalledProfile(catalog);
255
+ let profileSnapshotId;
256
+ const appliedPackages = [];
257
+ const failedPackages = [];
258
+ if (applying && profile.installed && profile.needsRefresh) {
259
+ const saved = (await readInstallState()).profile;
260
+ const prepared = await prepareCatalogInstall({
261
+ mode: saved.mode,
262
+ ...(saved.packageIds ? { packageIds: saved.packageIds } : {}),
263
+ }, {
264
+ requestedAgents: saved.agents,
265
+ catalog,
266
+ access: { modelApis: [] },
267
+ onProgress: printSetupProgress,
268
+ });
269
+ profileSnapshotId = await applyPreparedCatalogInstall(prepared, {
270
+ approveRisk: options.approveRisk,
271
+ });
272
+ if (!options.json)
273
+ console.log(`Applied reviewed ${saved.mode} profile changes. Snapshot: ${profileSnapshotId}`);
274
+ profile = await evaluateInstalledProfile(catalog);
275
+ }
276
+ if (!options.json)
277
+ console.error("Checking repository commits (4 at a time; changed sources may take up to 120s for safety review)…");
278
+ let plans = await buildUpdatePlan(undefined, {
279
+ packageId: options.package,
280
+ onProgress: options.json
281
+ ? undefined
282
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
283
+ });
284
+ if (options.package && plans.length === 0)
285
+ throw new Error(`No Loadout-managed installation named '${options.package}' was found. Run \`loadout list\` to see tracked packages.`);
286
+ if (applying) {
287
+ const safe = selectSafeAutomaticUpdates(plans);
288
+ for (const plan of safe)
289
+ try {
290
+ await applyPackageUpdate(plan.packageId);
291
+ appliedPackages.push(plan.packageId);
292
+ }
293
+ catch (error) {
294
+ failedPackages.push({
295
+ packageId: plan.packageId,
296
+ error: error instanceof Error ? error.message : String(error),
297
+ });
298
+ }
299
+ plans = await buildUpdatePlan(undefined, {
300
+ onProgress: options.json
301
+ ? undefined
302
+ : ({ completed, total, packageId }) => console.error(`✓ [${completed}/${total}] ${packageId}`),
303
+ });
304
+ if (!options.json) {
305
+ console.log(appliedPackages.length
306
+ ? `Updated safely: ${appliedPackages.join(", ")}`
307
+ : "No safe active package update needed applying.");
308
+ for (const item of failedPackages)
309
+ console.log(`Could not update ${item.packageId}: ${item.error}`);
310
+ const held = plans.filter((plan) => plan.status === "update-available" &&
311
+ (plan.approvalRequired || plan.disabledAgents?.length));
312
+ if (held.length)
313
+ console.log(`Held for explicit review: ${held.map((item) => item.packageId).join(", ")}`);
314
+ }
315
+ }
316
+ console.log(options.json
317
+ ? JSON.stringify({
318
+ profile,
319
+ packages: plans,
320
+ ...(applying
321
+ ? {
322
+ applied: {
323
+ profileSnapshotId,
324
+ packages: appliedPackages,
325
+ failures: failedPackages,
326
+ },
327
+ }
328
+ : {}),
329
+ }, null, 2)
330
+ : `${formatInstalledProfileStatus(profile)}\n\n${formatUpdatePlan(plans)}`);
331
+ });
332
+ program
333
+ .command("convert")
334
+ .description("Convert a subagent or hook into a static, loss-reported artifact")
335
+ .requiredOption("--kind <kind>", "subagent or hook")
336
+ .requiredOption("--target <target>", "codex-skill, claude-skill, or static-review")
337
+ .requiredOption("--name <name>", "source component name")
338
+ .requiredOption("--input <path>", "UTF-8 instruction or hook source file")
339
+ .requiredOption("--output <directory>", "artifact output directory")
340
+ .option("--yes", "write the artifact; otherwise preview only")
341
+ .option("--json", "emit machine-readable JSON")
342
+ .action(async (options) => {
343
+ const kinds = ["subagent", "hook"];
344
+ const targets = [
345
+ "codex-skill",
346
+ "claude-skill",
347
+ "static-review",
348
+ ];
349
+ if (!kinds.includes(options.kind))
350
+ throw new Error("--kind must be subagent or hook");
351
+ if (!targets.includes(options.target))
352
+ throw new Error("--target must be codex-skill, claude-skill, or static-review");
353
+ const result = compileConversion({
354
+ kind: options.kind,
355
+ name: options.name,
356
+ body: await readFile(options.input, "utf8"),
357
+ }, options.target);
358
+ const destination = resolve(options.output, result.relativePath);
359
+ if (options.yes)
360
+ await writeFileAtomically(destination, result.content, 0o600);
361
+ const report = {
362
+ ...result,
363
+ ...(options.yes
364
+ ? { destination }
365
+ : { destination, write: "preview-only" }),
366
+ };
367
+ console.log(options.json
368
+ ? JSON.stringify(report, null, 2)
369
+ : `${options.yes ? "Wrote" : "Previewed"} ${destination}.\n` +
370
+ `Preserved: ${result.preserved.join(", ")}. Dropped: ${result.dropped.length} field(s).\n` +
371
+ (result.requiresApproval
372
+ ? "Manual approval is required before using this artifact."
373
+ : "No additional approval is required."));
374
+ });
375
+ program
376
+ .command("canary")
377
+ .description("Run a static canary policy gate for a candidate without installing it")
378
+ .requiredOption("--source <directory>", "candidate package directory")
379
+ .requiredOption("--package <id>", "candidate package id")
380
+ .option("--repository <owner/repo>", "candidate repository")
381
+ .option("--commit <sha>", "candidate immutable commit")
382
+ .option("--approve", "approve promotion if a promotion callback is supplied")
383
+ .option("--allow-unready", "allow review findings in the static gate")
384
+ .option("--json", "emit machine-readable JSON")
385
+ .action(async (options) => {
386
+ const result = await runCanary({
387
+ packageId: options.package,
388
+ root: options.source,
389
+ ...(options.repository ? { repository: options.repository } : {}),
390
+ ...(options.commit ? { commit: options.commit } : {}),
391
+ }, { enabled: true, requireStaticReady: !options.allowUnready }, { approve: options.approve });
392
+ console.log(options.json
393
+ ? JSON.stringify(result, null, 2)
394
+ : formatCanaryResult(result));
395
+ if (result.status === "blocked")
396
+ process.exitCode = 1;
397
+ });
398
+ program
399
+ .command("serve")
400
+ .description("Start a loopback-only read-only API for status, health, catalog, and updates")
401
+ .option("--port <port>", "TCP port (0 selects an available port)", "0")
402
+ .action(async (options) => {
403
+ const handle = await startApiServer({ port: Number(options.port) });
404
+ console.log(`Loadout API listening at http://${handle.host}:${handle.port}`);
405
+ await new Promise((resolve) => {
406
+ process.once("SIGINT", resolve);
407
+ process.once("SIGTERM", resolve);
408
+ });
409
+ await handle.close();
410
+ });
411
+ }