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
@@ -0,0 +1,133 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readdir, rm, rmdir } from "node:fs/promises";
3
+ import { dirname, parse, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { loadoutHome } from "./paths.js";
6
+ import { applyRemove, planRemove } from "./remove.js";
7
+ import { applyRuntimeToolPlan, listInstalledRuntimeTools, planRuntimeTool, } from "./runtime-tools.js";
8
+ import { applyNativeSchedulerBundle, planNativeScheduler, } from "./scheduler.js";
9
+ import { readInstallState } from "./state.js";
10
+ export async function buildUninstallPlan(dependencies = {}) {
11
+ const stateHome = loadoutHome();
12
+ const state = await readInstallState();
13
+ const packageIds = [
14
+ ...new Set([
15
+ ...state.installs.map((install) => install.packageId),
16
+ ...(state.mcpInstalls ?? []).map((install) => install.packageId),
17
+ ]),
18
+ ];
19
+ const packages = await Promise.all(packageIds.map(planRemove));
20
+ const runtimeTools = await (dependencies.runtimeTools ?? listInstalledRuntimeTools)(stateHome);
21
+ const schedulers = dependencies.schedulerPlans?.() ??
22
+ ["updates", "discovery"].map((job) => planNativeScheduler("unschedule", { job }));
23
+ const warnings = packages.flatMap((entry) => entry.warnings);
24
+ if (runtimeTools.length)
25
+ warnings.push(`${runtimeTools.length} Loadout-managed runtime tool(s) will be restored to their pre-install snapshots.`);
26
+ warnings.push("Loadout's cache, disabled library, history, and rollback snapshots will be deleted. This final state cleanup cannot itself be rolled back.");
27
+ return {
28
+ stateHome,
29
+ packages,
30
+ runtimeTools,
31
+ schedulers,
32
+ disabledLibraryRecords: (state.activations ?? []).filter((entry) => entry.activationState === "disabled").length,
33
+ blocked: packages.some((entry) => entry.blocked),
34
+ warnings,
35
+ };
36
+ }
37
+ async function removeEmptyManagedDirectories(plans) {
38
+ const candidates = [
39
+ ...new Set(plans.flatMap((plan) => plan.files.map((file) => dirname(file.path)))),
40
+ ].sort((left, right) => right.length - left.length);
41
+ for (const directory of candidates) {
42
+ try {
43
+ const queue = [directory];
44
+ const visited = [directory];
45
+ let entriesChecked = 0;
46
+ let empty = true;
47
+ while (queue.length && empty) {
48
+ const current = queue.pop();
49
+ for (const entry of await readdir(current, { withFileTypes: true })) {
50
+ entriesChecked += 1;
51
+ if (entriesChecked > 10_000) {
52
+ empty = false;
53
+ break;
54
+ }
55
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
56
+ const child = resolve(current, entry.name);
57
+ queue.push(child);
58
+ visited.push(child);
59
+ }
60
+ else {
61
+ empty = false;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ if (empty)
67
+ for (const current of visited.sort((left, right) => right.length - left.length))
68
+ await rmdir(current);
69
+ }
70
+ catch {
71
+ // Missing and non-empty directories are both safe to leave alone.
72
+ }
73
+ }
74
+ }
75
+ function assertSafeStateHome(stateHome) {
76
+ const expected = resolve(loadoutHome());
77
+ const selected = resolve(stateHome);
78
+ const root = parse(selected).root;
79
+ if (selected !== expected || selected === root)
80
+ throw new Error(`Refusing unsafe Loadout state deletion target: ${selected}`);
81
+ }
82
+ export async function applyUninstall(plan, dependencies = {}, options = {}) {
83
+ assertSafeStateHome(plan.stateHome);
84
+ const fresh = await buildUninstallPlan(dependencies);
85
+ if (fresh.blocked && !options.force)
86
+ throw new Error(`Complete uninstall is blocked because managed files were modified. Review them, or re-run with --force. ${fresh.warnings.join(" ")}`);
87
+ for (const id of fresh.runtimeTools) {
88
+ const runtimePlan = await planRuntimeTool(id, {
89
+ action: "remove",
90
+ stateHome: fresh.stateHome,
91
+ });
92
+ await applyRuntimeToolPlan(runtimePlan, { approveRisk: true });
93
+ }
94
+ for (const packagePlan of fresh.packages)
95
+ await applyRemove(packagePlan, { force: options.force });
96
+ await removeEmptyManagedDirectories(fresh.packages);
97
+ if (fresh.schedulers.length)
98
+ await (dependencies.unschedule
99
+ ? dependencies.unschedule(fresh.schedulers)
100
+ : applyNativeSchedulerBundle(fresh.schedulers));
101
+ await rm(fresh.stateHome, { recursive: true, force: true });
102
+ return {
103
+ removedPackages: fresh.packages.length,
104
+ removedRuntimeTools: fresh.runtimeTools.length,
105
+ };
106
+ }
107
+ const execFileAsync = promisify(execFile);
108
+ /** Remove the globally installed npm launcher after managed data is gone. */
109
+ export async function uninstallGlobalCli() {
110
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
111
+ await execFileAsync(npm, ["uninstall", "--global", "loadout-ai"], {
112
+ windowsHide: true,
113
+ });
114
+ }
115
+ export function formatUninstallPlan(plan) {
116
+ return [
117
+ "Complete Loadout uninstall preview",
118
+ "",
119
+ `Managed packages: ${plan.packages.length}`,
120
+ `Managed runtime tools: ${plan.runtimeTools.length ? plan.runtimeTools.join(", ") : "none"}`,
121
+ `Disabled library records: ${plan.disabledLibraryRecords}`,
122
+ `Daily jobs to remove: ${plan.schedulers.map((item) => item.job).join(", ") || "none"}`,
123
+ `State and cache: ${plan.stateHome}`,
124
+ ...(plan.blocked
125
+ ? ["", "BLOCKED: managed files were changed outside Loadout."]
126
+ : []),
127
+ "",
128
+ ...plan.warnings.map((warning) => `Warning: ${warning}`),
129
+ "",
130
+ "Dry run only. Re-run with `loadout uninstall --yes` to remove Loadout-managed data.",
131
+ "Add `--remove-cli` to also uninstall the global npm command.",
132
+ ].join("\n");
133
+ }
@@ -85,66 +85,89 @@ async function analyzeManagedUpdate(oldRoot, newRoot, unitIds) {
85
85
  };
86
86
  }
87
87
  /** Builds a read-only update plan from persisted installs and live GitHub snapshots. */
88
- export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository)) {
88
+ export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository, { timeoutMs: 30_000 }), options = {}) {
89
89
  const state = await readInstallState();
90
- return Promise.all(state.installs.map(async (record) => {
91
- const disabledAgents = (state.activations ?? [])
92
- .filter((activation) => activation.packageId === record.packageId &&
93
- activation.installationState === "installed" &&
94
- activation.activationState === "disabled")
95
- .map((activation) => activation.agent);
96
- const base = {
97
- packageId: record.packageId,
98
- repository: record.repository,
99
- installedCommit: record.resolvedCommit,
100
- targetAgents: record.targetAgents,
101
- ...(disabledAgents.length ? { disabledAgents } : {}),
102
- };
103
- if (!record.repository || !record.resolvedCommit) {
104
- return {
105
- ...base,
106
- status: "untracked",
107
- action: "Reinstall from the original source to begin update tracking.",
108
- };
109
- }
110
- try {
111
- const current = await resolver(record.repository);
112
- const same = current.commit.toLowerCase() === record.resolvedCommit.toLowerCase();
113
- let diff;
114
- let safetyFindings;
115
- let approvalRequired = false;
116
- if (!same && current.path) {
117
- const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
118
- const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
119
- diff = analysis.diff;
120
- safetyFindings = analysis.safetyFindings;
121
- approvalRequired = analysis.approvalRequired;
122
- }
123
- return {
124
- ...base,
125
- availableCommit: current.commit,
126
- status: same ? "up-to-date" : "update-available",
127
- action: same
128
- ? "No action required."
129
- : disabledAgents.length
130
- ? `Enable ${record.packageId} for ${disabledAgents.join(", ")} before applying an update; planning remains read-only.`
131
- : approvalRequired
132
- ? `Approval required: review safety warnings before updating ${record.packageId}.`
133
- : `Run loadout update --package ${record.packageId} after review.`,
134
- ...(approvalRequired ? { approvalRequired: true } : {}),
135
- ...(safetyFindings?.length ? { safetyFindings } : {}),
136
- ...(diff ? { diff } : {}),
137
- };
138
- }
139
- catch (error) {
140
- return {
141
- ...base,
142
- status: "error",
143
- action: "Retry when GitHub is reachable; the installed version was not changed.",
144
- error: error instanceof Error ? error.message : String(error),
145
- };
90
+ const records = options.packageId
91
+ ? state.installs.filter((record) => record.packageId === options.packageId)
92
+ : state.installs;
93
+ const results = new Array(records.length);
94
+ let cursor = 0;
95
+ let completed = 0;
96
+ const workers = Array.from({
97
+ length: Math.min(Math.max(1, options.concurrency ?? 4), Math.max(1, records.length)),
98
+ }, async () => {
99
+ while (cursor < records.length) {
100
+ const index = cursor++;
101
+ const record = records[index];
102
+ results[index] = await (async () => {
103
+ const disabledAgents = (state.activations ?? [])
104
+ .filter((activation) => activation.packageId === record.packageId &&
105
+ activation.installationState === "installed" &&
106
+ activation.activationState === "disabled")
107
+ .map((activation) => activation.agent);
108
+ const base = {
109
+ packageId: record.packageId,
110
+ repository: record.repository,
111
+ installedCommit: record.resolvedCommit,
112
+ targetAgents: record.targetAgents,
113
+ ...(disabledAgents.length ? { disabledAgents } : {}),
114
+ };
115
+ if (!record.repository || !record.resolvedCommit) {
116
+ return {
117
+ ...base,
118
+ status: "untracked",
119
+ action: "Reinstall from the original source to begin update tracking.",
120
+ };
121
+ }
122
+ try {
123
+ const current = await resolver(record.repository);
124
+ const same = current.commit.toLowerCase() ===
125
+ record.resolvedCommit.toLowerCase();
126
+ let diff;
127
+ let safetyFindings;
128
+ let approvalRequired = false;
129
+ if (!same && current.path) {
130
+ const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
131
+ const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
132
+ diff = analysis.diff;
133
+ safetyFindings = analysis.safetyFindings;
134
+ approvalRequired = analysis.approvalRequired;
135
+ }
136
+ return {
137
+ ...base,
138
+ availableCommit: current.commit,
139
+ status: same ? "up-to-date" : "update-available",
140
+ action: same
141
+ ? "No action required."
142
+ : disabledAgents.length
143
+ ? `Enable ${record.packageId} for ${disabledAgents.join(", ")} before applying an update; planning remains read-only.`
144
+ : approvalRequired
145
+ ? `Approval required: review safety warnings before updating ${record.packageId}.`
146
+ : `Run loadout update --package ${record.packageId} --yes after review.`,
147
+ ...(approvalRequired ? { approvalRequired: true } : {}),
148
+ ...(safetyFindings?.length ? { safetyFindings } : {}),
149
+ ...(diff ? { diff } : {}),
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ ...base,
155
+ status: "error",
156
+ action: "Retry when GitHub is reachable; the installed version was not changed.",
157
+ error: error instanceof Error ? error.message : String(error),
158
+ };
159
+ }
160
+ })();
161
+ completed += 1;
162
+ options.onProgress?.({
163
+ completed,
164
+ total: records.length,
165
+ packageId: record.packageId,
166
+ });
146
167
  }
147
- }));
168
+ });
169
+ await Promise.all(workers);
170
+ return results;
148
171
  }
149
172
  export function formatUpdatePlan(plans) {
150
173
  if (plans.length === 0)
@@ -160,6 +183,12 @@ export function formatUpdatePlan(plans) {
160
183
  })
161
184
  .join("\n");
162
185
  }
186
+ /** Updates safe enough for an explicit whole-profile `update --yes` apply. */
187
+ export function selectSafeAutomaticUpdates(plans) {
188
+ return plans.filter((plan) => plan.status === "update-available" &&
189
+ !plan.approvalRequired &&
190
+ !plan.disabledAgents?.length);
191
+ }
163
192
  function quarantineRoot() {
164
193
  return join(loadoutHome(), "quarantine");
165
194
  }
@@ -8,7 +8,7 @@ import { inspectAgents } from "./core/agent-inspection.js";
8
8
  import { loadEffectiveCatalog, rankCatalog } from "./core/catalog.js";
9
9
  import { buildUpdatePlan } from "./core/update.js";
10
10
  import { buildHealthReport } from "./core/health.js";
11
- import { recommendPackages, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
11
+ import { recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
12
12
  import { searchLocalRegistry } from "./core/registry.js";
13
13
  import { randomBytes, timingSafeEqual } from "node:crypto";
14
14
  import { applySyncPlan, buildSyncPlan } from "./core/sync.js";
@@ -318,6 +318,7 @@ async function route(request, response, context) {
318
318
  const signals = await scanProject(process.cwd());
319
319
  await sendJson(response, 200, {
320
320
  signals,
321
+ recommendationBoundary: RECOMMENDATION_BOUNDARY,
321
322
  recommendations: recommendPackages(signals, await loadEffectiveCatalog()),
322
323
  });
323
324
  return;
@@ -368,7 +369,9 @@ export function createDashboardServer(options = {}) {
368
369
  buildSync: options.buildSync ?? buildSyncPlan,
369
370
  applySync: options.applySync ?? applySyncPlan,
370
371
  rollback: options.rollback ??
371
- (async (snapshotId) => withMutationLock(async () => restoreSnapshot(await readSnapshot(snapshotId)))),
372
+ (async (snapshotId) => withMutationLock(async () => restoreSnapshot(await readSnapshot(snapshotId), {
373
+ requireUnchangedPostMutationState: true,
374
+ }))),
372
375
  token: randomBytes(32).toString("hex"),
373
376
  };
374
377
  return createServer((request, response) => {
@@ -41,8 +41,26 @@ export const componentCompatibilitySchema = z.enum([
41
41
  "unsupported",
42
42
  ]);
43
43
  export const safetyRiskLevelSchema = z.enum(["safe", "review", "blocked"]);
44
+ export const readmeClaimEvidenceClassSchema = z.enum([
45
+ "structural",
46
+ "unit-verified",
47
+ "integration-verified",
48
+ "live-verified",
49
+ "platform-verified",
50
+ "human-reviewed",
51
+ "benchmarked",
52
+ "policy-selected",
53
+ ]);
54
+ export const readmeClaimStatusSchema = z.enum([
55
+ "proven",
56
+ "bounded",
57
+ "unfulfilled",
58
+ ]);
44
59
  const text = z.string().trim().min(1, "must not be empty");
45
60
  const optionalText = text.optional();
61
+ const readmeClaimIdSchema = z
62
+ .string()
63
+ .regex(/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/, "must be a safe dotted identifier");
46
64
  const sha256 = z.string().regex(/^[a-f0-9]{64}$/i, "must be a SHA-256 hash");
47
65
  const gitSha = z.string().regex(/^[a-f0-9]{40}$/i, "must be a full Git SHA");
48
66
  const repository = z
@@ -304,6 +322,18 @@ export const installStateSchema = z
304
322
  installs: z.array(installRecordSchema),
305
323
  mcpInstalls: z.array(mcpInstallRecordSchema).default([]),
306
324
  activations: z.array(managedActivationRecordSchema).default([]),
325
+ profile: z
326
+ .object({
327
+ mode: z.enum(["stable", "power", "maximum", "custom"]),
328
+ packageIds: z.array(text).optional(),
329
+ agents: z.array(agentIdSchema),
330
+ catalogPackages: z.array(z.object({
331
+ packageId: text,
332
+ reviewedCommit: optionalText,
333
+ })),
334
+ appliedAt: text,
335
+ })
336
+ .optional(),
307
337
  })
308
338
  .passthrough();
309
339
  const lockedPackageSchema = z
@@ -336,6 +366,45 @@ export const loadoutLockfileSchema = z
336
366
  .optional(),
337
367
  })
338
368
  .passthrough();
369
+ export const readmeClaimSchema = z
370
+ .object({
371
+ id: readmeClaimIdSchema,
372
+ section: text,
373
+ summary: text,
374
+ evidenceClass: readmeClaimEvidenceClassSchema,
375
+ status: readmeClaimStatusSchema,
376
+ evidence: z.array(text),
377
+ externalPrerequisites: z.array(text).optional(),
378
+ })
379
+ .strict()
380
+ .superRefine((claim, context) => {
381
+ if (claim.status === "proven" && claim.evidence.length === 0) {
382
+ context.addIssue({
383
+ code: "custom",
384
+ path: ["evidence"],
385
+ message: "proven claims require at least one authoritative evidence reference",
386
+ });
387
+ }
388
+ });
389
+ export const readmeClaimManifestSchema = z
390
+ .object({
391
+ schemaVersion: z.literal(1),
392
+ claims: z.array(readmeClaimSchema).min(1),
393
+ })
394
+ .strict()
395
+ .superRefine((manifest, context) => {
396
+ const seen = new Set();
397
+ for (const [index, claim] of manifest.claims.entries()) {
398
+ if (seen.has(claim.id)) {
399
+ context.addIssue({
400
+ code: "custom",
401
+ path: ["claims", index, "id"],
402
+ message: "must be unique",
403
+ });
404
+ }
405
+ seen.add(claim.id);
406
+ }
407
+ });
339
408
  /** Compact, path-aware errors suitable for CLI and persisted-data diagnostics. */
340
409
  export function formatSchemaError(error) {
341
410
  return error.issues
@@ -121,8 +121,10 @@ reruns and diagnosis.
121
121
  | `npm run format:check` | Repository formatting | Exit 0; no files changed. |
122
122
  | `npm run lint` | TypeScript lint rules | Exit 0. |
123
123
  | `npm run typecheck` | TypeScript contract | Exit 0. |
124
+ | `npm run check:evidence` | Catalog/discovery attribution, README claims, and release boundaries | Exit 0; no claim is silently promoted. |
124
125
  | `npm test` | Unit, integration, native filesystem, safety, and regression suites | All tests pass. |
125
126
  | `npm run test:e2e:cli` | Disposable scan → compare → optimize → apply → rollback journey | Prints a successful CLI product flow. |
127
+ | `npm run test:e2e:readme` | Isolated library/activation/manifest/card/rollback journey | Prints README product flow success. |
126
128
  | `npm run test:package` | `npm pack`, install outside the checkout, packaged CLI install/rollback | Prints package smoke success. |
127
129
  | `npm run test:performance` | Seven scans of 1,000 real on-disk skill directories | p95 remains below the enforced five-second budget. |
128
130
  | `npm run test:e2e:dashboard` | Loopback dashboard first-run browser test | Playwright passes; no real profile is used. |
@@ -131,6 +133,20 @@ The dashboard test needs a Playwright browser. If the browser executable is abse
131
133
  run `npx playwright install chromium` once; that download is not a Loadout product
132
134
  side effect.
133
135
 
136
+ The focused regression contract for the v0.3.x profile lifecycle is:
137
+
138
+ ```bash
139
+ npx vitest run tests/upgrade.test.ts tests/profile-state.test.ts \
140
+ tests/update.test.ts tests/uninstall.test.ts tests/install.test.ts \
141
+ tests/mcp-recipes.test.ts tests/cli-help.test.ts
142
+ ```
143
+
144
+ It verifies preview/apply upgrade behavior, saved-profile refresh detection, safe
145
+ profile reconciliation, complete-uninstall drift protection, recursively empty target
146
+ recovery, and the separation between model-provider access declarations and service
147
+ credentials. These are disposable automated filesystem tests; they do not prove that
148
+ a native agent consumes the resulting files or that a host scheduler/keychain works.
149
+
134
150
  ## 2. Read-only inventory, ranking, and recommendation track (R; some N)
135
151
 
136
152
  ```bash
@@ -0,0 +1,36 @@
1
+ # README redesign research
2
+
3
+ Research was performed against the current default-branch READMEs on 2026-07-19 and
4
+ recorded at immutable commits so the references remain reproducible.
5
+
6
+ | Repository | README studied | Principle adopted |
7
+ | ---------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
8
+ | Ponytail | [`16f2980`](https://github.com/DietrichGebert/ponytail/blob/16f29800fd2681bdf24f3eb4ccffe38be3baec6b/README.md) | Give the tool an unmistakable identity and memorable line. |
9
+ | uv | [`1535a67`](https://github.com/astral-sh/uv/blob/1535a6767e5ebd77eac2ace0f6cf1a3edc5f681c/README.md) | Define the product immediately, then show installation and observable proof. |
10
+ | bat | [`7895139`](https://github.com/sharkdp/bat/blob/78951393e29bfd2f2a45f4326b9d2bb5e737dd2a/README.md) | Demonstrate the terminal experience before exhaustive platform detail. |
11
+ | fzf | [`b163463`](https://github.com/junegunn/fzf/blob/b163463079e6254b8582b05acefcf187ec160d9b/README.md) | Use recognizable branding and compact capability statements. |
12
+ | ripgrep | [`227381d`](https://github.com/BurntSushi/ripgrep/blob/227381db0ee83dfa4341f1e27ff9617c0f5ad992/README.md) | Prefer technical precision, scoped proof, and explicit limitations. |
13
+ | mise | [`126e775`](https://github.com/jdx/mise/blob/126e7755cc22e36c3d206b650de613951146b5e3/README.md) | Pair a concise purpose with a real demo and copyable quickstart. |
14
+ | Gum | [`716d8b5`](https://github.com/charmbracelet/gum/blob/716d8b5d0221558f944b5a078dbbcca8572534fb/README.md) | Teach one complete use case before listing the command surface. |
15
+ | Starship | [`8f28dfc`](https://github.com/starship/starship/blob/8f28dfcb1ca3242fba00a3cf98c10ee24605c3ed/README.md) | Separate prerequisites, installation, and configuration. |
16
+
17
+ ## Adopted
18
+
19
+ - A small original mark and one memorable product line.
20
+ - A proof-first opening with only CI, Node requirement, and license badges.
21
+ - A real terminal transcript that distinguishes preview from mutation.
22
+ - Installation and a disposable first success near the top.
23
+ - Short summaries with direct links to detailed technical evidence.
24
+ - Explicit boundaries beside the claims they qualify.
25
+
26
+ ## Rejected
27
+
28
+ - Copying another project's mascot, artwork, prose, or layout.
29
+ - Comparative performance charts or speed claims; Loadout has no valid competitor
30
+ benchmark.
31
+ - Screenshot-led presentation without a real product screenshot.
32
+ - `npm install --global loadout-ai@0.3.2`; that version is not currently published.
33
+ - Claims of universal safety, production readiness, human review, benchmarked sources,
34
+ or native execution across every configured agent.
35
+ - Badge arrays, star counters, community/sponsor promotion, animations, and exhaustive
36
+ command or platform tables on the front page.
@@ -1,8 +1,34 @@
1
- # Release review — 2026-07-15
2
-
3
- This review covers the current Loadout implementation, not an aspirational
4
- roadmap. It was performed after the transaction, source-fetch, dashboard, and
5
- adapter test suites passed locally.
1
+ # Release review — historical 2026-07-15 review, updated 2026-07-19
2
+
3
+ ## Current status and evidence boundary
4
+
5
+ The sections below preserve the evidence recorded for the earlier 0.1.0 review; their
6
+ old package version, catalog count, and test totals are historical and must not be read
7
+ as current 0.3.2 results. The checked-in package is now 0.3.2 with a 50-record catalog.
8
+
9
+ On 2026-07-19, the focused v0.3.x regression run passed 58 tests covering the unified
10
+ upgrade, saved-profile updates, complete uninstall, separation of model API access from
11
+ service credentials, and recursively empty skill-directory recovery. The later
12
+ `npm run verify:full` result is bound to exact tested commit
13
+ `8f8eccdd20272ebb88d0339087fc9cd3828e65c9`: its deterministic evidence gate, 552 tests
14
+ with one explicit skip, both CLI product journeys, package smoke, the 1,000-skill
15
+ performance gate, and two Playwright dashboard projects passed. The evidence-only
16
+ follow-up commit that records this statement was not represented as part of that tested
17
+ commit. These local results establish the tested repository behaviors only; they do not
18
+ retroactively establish native-agent recognition, current npm publication, branch
19
+ protection, or an independent security review.
20
+
21
+ The separate [sanitized live-check report](./evidence/live-checks-2026-07-19.json) was
22
+ generated at `2026-07-19T13:45:14.945Z` and records the same repository commit
23
+ `8f8eccdd20272ebb88d0339087fc9cd3828e65c9` as the deterministic run above. At that
24
+ historical observation time, the pinned Stable install and rollback were verified; npm
25
+ returned 404 for `loadout-ai@0.3.2`; and authenticated GitHub access reached the
26
+ repository but branch protection for `main` returned 404. These results can change
27
+ after the timestamp and are not part of the deterministic offline gate.
28
+
29
+ At the time it was written, this review covered the then-current Loadout
30
+ implementation, not an aspirational roadmap. It was performed after the transaction,
31
+ source-fetch, dashboard, and adapter test suites passed locally.
6
32
 
7
33
  ## P4-08: atomic-commit review — accepted with explicit durability boundary
8
34