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.
- package/CHANGELOG.md +71 -0
- package/MASTER_PLAN.md +141 -33
- package/README.md +160 -255
- package/dashboard/app.js +4 -4
- package/dashboard/index.html +4 -4
- package/dist/src/cli.js +189 -32
- package/dist/src/core/active-set.js +37 -2
- package/dist/src/core/adapters.js +10 -0
- package/dist/src/core/adopt.js +165 -32
- package/dist/src/core/agent-health-score.js +2 -2
- package/dist/src/core/catalog-coverage.js +2 -1
- package/dist/src/core/catalog-install.js +8 -1
- package/dist/src/core/catalog-release.js +2 -1
- package/dist/src/core/cli-guide.js +101 -0
- package/dist/src/core/completion.js +3 -0
- package/dist/src/core/conformance.js +74 -0
- package/dist/src/core/install.js +36 -3
- package/dist/src/core/mcp-recipes.js +21 -0
- package/dist/src/core/profile-state.js +101 -0
- package/dist/src/core/profiles.js +9 -4
- package/dist/src/core/ranking.js +1 -1
- package/dist/src/core/readme-claims.js +10 -0
- package/dist/src/core/readme-facts.js +40 -0
- package/dist/src/core/recommend.js +9 -3
- package/dist/src/core/remove.js +6 -7
- package/dist/src/core/runtime-tools.js +10 -2
- package/dist/src/core/scheduler.js +4 -3
- package/dist/src/core/snapshot.js +58 -13
- package/dist/src/core/state.js +13 -3
- package/dist/src/core/transaction.js +2 -1
- package/dist/src/core/uninstall.js +133 -0
- package/dist/src/core/update.js +87 -58
- package/dist/src/dashboard.js +5 -2
- package/dist/src/shared/schemas.js +69 -0
- package/docs/FEATURE_TEST_MATRIX.md +16 -0
- package/docs/README_RESEARCH.md +36 -0
- package/docs/RELEASE_REVIEW.md +31 -5
- package/docs/REPOSITORY_STABILIZATION.md +190 -0
- package/docs/TESTING.md +75 -2
- package/docs/USER_TEST_GUIDE.md +202 -0
- package/docs/assets/loadout-hero.svg +259 -0
- package/docs/assets/loadout-mark.svg +54 -0
- package/docs/evidence/live-checks-2026-07-19.json +22 -0
- package/docs/evidence/live-checks.schema.json +28 -0
- package/docs/evidence/readme-claims.json +286 -0
- package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +283 -0
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +80 -0
- package/package.json +8 -4
- 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
|
+
}
|
package/dist/src/core/update.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|
package/dist/src/dashboard.js
CHANGED
|
@@ -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.
|
package/docs/RELEASE_REVIEW.md
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
# Release review — 2026-07-15
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|