loadout-ai 0.4.0 → 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 (48) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/MASTER_PLAN.md +173 -9
  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 +216 -42
  8. package/dist/src/core/active-set.js +14 -5
  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 +6 -39
  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 +129 -27
  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/target-occupancy.js +50 -0
  29. package/dist/src/core/transaction.js +2 -1
  30. package/dist/src/shared/schemas.js +1 -0
  31. package/docs/CATALOG.md +3 -2
  32. package/docs/DISCOVERED.md +253 -255
  33. package/docs/FEATURE_TEST_MATRIX.md +19 -32
  34. package/docs/GITHUB_AUTHORIZATION.md +2 -2
  35. package/docs/TESTING.md +3 -16
  36. package/docs/USER_TEST_GUIDE.md +39 -13
  37. package/docs/assets/loadout-workflow.png +0 -0
  38. package/docs/evidence/readme-claims.json +0 -8
  39. package/docs/superpowers/plans/2026-07-20-loadout-readme-explainer.md +116 -0
  40. package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +469 -0
  41. package/docs/superpowers/specs/2026-07-20-loadout-readme-explainer-design.md +55 -0
  42. package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +228 -0
  43. package/package.json +4 -6
  44. package/dashboard/app.js +0 -607
  45. package/dashboard/index.html +0 -249
  46. package/dashboard/styles.css +0 -384
  47. package/dist/src/core/demo.js +0 -136
  48. package/dist/src/dashboard.js +0 -421
@@ -10,6 +10,7 @@ export async function createSnapshot(paths, options = {}) {
10
10
  .digest("hex")
11
11
  .slice(0, 12)}`,
12
12
  createdAt: new Date().toISOString(),
13
+ ...(options.label ? { label: options.label } : {}),
13
14
  roots: normalizedRoots,
14
15
  files: [],
15
16
  };
@@ -154,6 +155,22 @@ export async function listSnapshotIds() {
154
155
  throw error;
155
156
  }
156
157
  }
158
+ function changedEntryCount(before, after) {
159
+ if (!after)
160
+ return undefined;
161
+ const previous = new Map(before.map((file) => [file.path, file]));
162
+ const next = new Map(after.map((file) => [file.path, file]));
163
+ return [...new Set([...previous.keys(), ...next.keys()])].filter((path) => JSON.stringify(previous.get(path)) !== JSON.stringify(next.get(path))).length;
164
+ }
165
+ export function summarizeSnapshot(snapshot) {
166
+ return {
167
+ id: snapshot.id,
168
+ createdAt: snapshot.createdAt,
169
+ label: snapshot.label ?? "managed change",
170
+ roots: snapshot.roots.length,
171
+ changedEntries: changedEntryCount(snapshot.files, snapshot.postMutationFiles),
172
+ };
173
+ }
157
174
  const SNAPSHOT_ID = /^\d{10,}-[a-f0-9]{12}$/;
158
175
  function isSnapshotId(value) {
159
176
  return SNAPSHOT_ID.test(value);
@@ -186,6 +203,9 @@ export function validateSnapshot(value) {
186
203
  if (typeof value.createdAt !== "string" ||
187
204
  !Number.isFinite(Date.parse(value.createdAt)))
188
205
  throw new Error("Snapshot creation time is invalid");
206
+ if (value.label !== undefined &&
207
+ (typeof value.label !== "string" || value.label.trim().length === 0))
208
+ throw new Error("Snapshot label is invalid");
189
209
  if (!Array.isArray(value.roots) ||
190
210
  value.roots.length === 0 ||
191
211
  !value.roots.every((root) => typeof root === "string" && root.length > 0))
@@ -302,6 +302,15 @@ export async function recordInstallTransaction(entries, mcpEntries, snapshotId)
302
302
  await writeInstallState(state);
303
303
  return { installs, mcpInstalls };
304
304
  }
305
+ /** Record one externally formatted but Loadout-managed MCP entry. */
306
+ export async function recordMcpInstall(record) {
307
+ const state = await readInstallState();
308
+ state.mcpInstalls = [
309
+ ...(state.mcpInstalls ?? []).filter((entry) => entry.packageId !== record.packageId),
310
+ record,
311
+ ];
312
+ await writeInstallState(state);
313
+ }
305
314
  export async function forgetInstall(packageId) {
306
315
  const state = await readInstallState();
307
316
  const installs = state.installs.filter((entry) => entry.packageId !== packageId);
@@ -343,6 +343,6 @@ export async function applySyncPlan(plan, lockPath = "loadout.lock", options = {
343
343
  await writeMcpConfigPlan(entry.plan);
344
344
  await recordInstallTransaction(freshPlan.packages, freshPlan.mcpPlans, snapshot.id);
345
345
  await writeLockfile(freshPlan.resolvedManifest ?? (await readManifest(freshPlan.manifest)), lockPath);
346
- });
346
+ }, { label: "synchronize loadout manifest" });
347
347
  return { snapshotId: applied.snapshotId, lockfile: lockPath };
348
348
  }
@@ -0,0 +1,50 @@
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Inspect a prospective skill target without following symlinks or executing
5
+ * any content. Missing paths and recursively empty directories are safe to
6
+ * replace; every uncertain state fails closed.
7
+ */
8
+ export async function inspectTargetOccupancy(path, maximumEntries = 10_000) {
9
+ let root;
10
+ try {
11
+ root = await lstat(path);
12
+ }
13
+ catch (error) {
14
+ if (error &&
15
+ typeof error === "object" &&
16
+ "code" in error &&
17
+ error.code === "ENOENT")
18
+ return { occupied: false };
19
+ return { occupied: true, reason: "unreadable" };
20
+ }
21
+ if (root.isSymbolicLink())
22
+ return { occupied: true, reason: "symlink" };
23
+ if (!root.isDirectory())
24
+ return { occupied: true, reason: "unsupported" };
25
+ const queue = [path];
26
+ let inspected = 0;
27
+ while (queue.length) {
28
+ const directory = queue.pop();
29
+ let entries;
30
+ try {
31
+ entries = await readdir(directory, { withFileTypes: true });
32
+ }
33
+ catch {
34
+ return { occupied: true, reason: "unreadable" };
35
+ }
36
+ for (const entry of entries) {
37
+ inspected += 1;
38
+ if (inspected > maximumEntries)
39
+ return { occupied: true, reason: "inspection-limit" };
40
+ if (entry.isDirectory() && !entry.isSymbolicLink())
41
+ queue.push(join(directory, entry.name));
42
+ else
43
+ return {
44
+ occupied: true,
45
+ reason: entry.isSymbolicLink() ? "symlink" : "content",
46
+ };
47
+ }
48
+ }
49
+ return { occupied: false };
50
+ }
@@ -123,13 +123,14 @@ export async function withMutationLock(operation) {
123
123
  * global lock. Network/source preparation should happen before this boundary;
124
124
  * every state-dependent precondition belongs in `prepare`.
125
125
  */
126
- export async function runMutationTransaction(prepare, mutate) {
126
+ export async function runMutationTransaction(prepare, mutate, options = {}) {
127
127
  const mutationLock = await acquireMutationLock();
128
128
  let transaction;
129
129
  try {
130
130
  const prepared = await prepare();
131
131
  const snapshot = await createSnapshot(prepared.targets, {
132
132
  persist: false,
133
+ label: options.label,
133
134
  });
134
135
  transaction = await beginTransaction(snapshot, prepared.targets, {
135
136
  mutationLock,
@@ -275,6 +275,7 @@ export const installRecordSchema = z
275
275
  files: z.array(fileHashSchema),
276
276
  snapshotId: text,
277
277
  installedAt: text,
278
+ configFormat: z.enum(["json", "codex-toml"]).optional(),
278
279
  staticAssessment: z
279
280
  .object({
280
281
  status: z.enum(["clear", "warning", "blocking"]),
package/docs/CATALOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Catalog and upstream credits
2
2
 
3
- Loadout's bundled catalog indexes 50 public upstream repositories. This page gives every project a direct source link and exposes the exact reviewed revision used by Loadout.
3
+ Loadout's bundled catalog indexes 51 public upstream repositories. This page gives every project a direct source link and exposes the exact reviewed revision used by Loadout.
4
4
 
5
5
  Thank you to every maintainer and contributor whose work appears here. Loadout does not own, endorse for its authors, or relicense these projects. Each project remains governed by its upstream license and current terms.
6
6
 
@@ -14,7 +14,7 @@ Thank you to every maintainer and contributor whose work appears here. Loadout d
14
14
 
15
15
  The machine-readable catalog remains the source of truth. Run `loadout catalog --coverage --json` to inspect coverage and `loadout capabilities` to inspect what each local agent adapter can actually manage.
16
16
 
17
- ## All 50 credited repositories
17
+ ## All 51 credited repositories
18
18
 
19
19
  | Upstream source | Loadout ID | Category | Reviewed components | License status | Reviewed revision |
20
20
  | --------------------------------------------------------------------------------- | -------------------------------- | ------------------------ | -------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
@@ -68,6 +68,7 @@ The machine-readable catalog remains the source of truth. Run `loadout catalog -
68
68
  | [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) | `mcp-toolbox-for-databases` | database | `mcp` | `Apache-2.0` | [`b574b071fc27`](https://github.com/googleapis/mcp-toolbox/tree/b574b071fc27f717e90b4362864b1086be0c8995) |
69
69
  | [Browserbase MCP Server](https://github.com/browserbase/mcp-server-browserbase) | `browserbase-mcp-server` | browser-automation | `mcp` | `Apache-2.0` | [`1e196b3d3c4d`](https://github.com/browserbase/mcp-server-browserbase/tree/1e196b3d3c4dc70944e0d19038dd9eb3608b207a) |
70
70
  | [Bright Data MCP](https://github.com/brightdata/brightdata-mcp) | `bright-data-mcp` | web-research | `mcp` | `MIT` | [`adbfdd49b880`](https://github.com/brightdata/brightdata-mcp/tree/adbfdd49b8805136662a264490118c2688da3e29) |
71
+ | [Obsidian Skills](https://github.com/kepano/obsidian-skills) | `obsidian-skills` | knowledge-management | `skill`, `plugin` | `MIT` | [`a1dc48e68138`](https://github.com/kepano/obsidian-skills/tree/a1dc48e68138490d522c04cbf5822214c6eb1202) |
71
72
 
72
73
  ## Catalog maintenance
73
74