loadout-ai 0.1.2 → 0.2.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 +24 -0
- package/MASTER_PLAN.md +79 -0
- package/README.md +218 -330
- package/catalog/discovered.json +7925 -6183
- package/dist/src/cli.js +52 -27
- package/dist/src/core/access.js +42 -0
- package/dist/src/core/catalog-install.js +60 -12
- package/dist/src/core/health-score-evidence.js +8 -0
- package/dist/src/core/install.js +15 -12
- package/dist/src/core/mcp-recipes.js +37 -6
- package/dist/src/core/skills.js +20 -7
- package/dist/src/core/state.js +20 -4
- package/dist/src/core/update.js +111 -10
- package/dist/src/core/upgrade.js +1 -0
- package/dist/src/shared/schemas.js +8 -0
- package/docs/DISCOVERED.md +253 -253
- package/docs/FEATURE_TEST_MATRIX.md +8 -7
- package/docs/TESTING.md +44 -2
- package/package.json +2 -1
package/dist/src/core/update.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { fetchRepositorySnapshot } from "./source.js";
|
|
4
4
|
import { repositoryCachePath } from "./source.js";
|
|
5
5
|
import { diffRepositorySnapshots } from "./diff.js";
|
|
@@ -7,7 +7,83 @@ import { hashDirectory, readInstallState } from "./state.js";
|
|
|
7
7
|
import { analyzeUpdateSafety } from "./safety.js";
|
|
8
8
|
import { detectAgents, loadoutHome } from "./paths.js";
|
|
9
9
|
import { applySkillInstall, buildSkillPlan, installedAgents, } from "./install.js";
|
|
10
|
-
import { validateSkillDirectory } from "./skills.js";
|
|
10
|
+
import { discoverSkillDirectories, validateSkillDirectory, } from "./skills.js";
|
|
11
|
+
function managedUnitIds(state, packageId) {
|
|
12
|
+
return [
|
|
13
|
+
...new Set((state.activations ?? [])
|
|
14
|
+
.filter((activation) => activation.packageId === packageId &&
|
|
15
|
+
activation.installationState === "installed")
|
|
16
|
+
.map((activation) => activation.unitId ??
|
|
17
|
+
basename(activation.targets[0]?.activePath ?? packageId))
|
|
18
|
+
.filter(Boolean)),
|
|
19
|
+
].sort();
|
|
20
|
+
}
|
|
21
|
+
async function locateManagedSkills(root, unitIds) {
|
|
22
|
+
const normalized = new Map(unitIds.map((unitId) => [unitId.toLowerCase(), unitId]));
|
|
23
|
+
const located = new Map();
|
|
24
|
+
await discoverSkillDirectories(root, {
|
|
25
|
+
validate: false,
|
|
26
|
+
include: (skill) => {
|
|
27
|
+
const unitId = (skill.name ? normalized.get(skill.name.toLowerCase()) : undefined) ??
|
|
28
|
+
normalized.get(skill.targetName.toLowerCase());
|
|
29
|
+
if (!unitId)
|
|
30
|
+
return false;
|
|
31
|
+
located.set(unitId, skill.path);
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return located;
|
|
36
|
+
}
|
|
37
|
+
async function analyzeManagedUpdate(oldRoot, newRoot, unitIds) {
|
|
38
|
+
if (!unitIds.length) {
|
|
39
|
+
const [diff, safety] = await Promise.all([
|
|
40
|
+
diffRepositorySnapshots(oldRoot, newRoot),
|
|
41
|
+
analyzeUpdateSafety(oldRoot, newRoot),
|
|
42
|
+
]);
|
|
43
|
+
return {
|
|
44
|
+
diff,
|
|
45
|
+
safetyFindings: safety.findings,
|
|
46
|
+
approvalRequired: safety.approvalRequired,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const [oldSkills, newSkills] = await Promise.all([
|
|
50
|
+
locateManagedSkills(oldRoot, unitIds),
|
|
51
|
+
locateManagedSkills(newRoot, unitIds),
|
|
52
|
+
]);
|
|
53
|
+
const diff = [];
|
|
54
|
+
const findings = [];
|
|
55
|
+
for (const unitId of unitIds) {
|
|
56
|
+
const oldPath = oldSkills.get(unitId);
|
|
57
|
+
const newPath = newSkills.get(unitId);
|
|
58
|
+
if (!oldPath || !newPath) {
|
|
59
|
+
findings.push({
|
|
60
|
+
severity: "blocking",
|
|
61
|
+
category: "instruction",
|
|
62
|
+
message: `Managed skill '${unitId}' is missing from one update revision.`,
|
|
63
|
+
paths: [unitId],
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
diff.push(...(await diffRepositorySnapshots(oldPath, newPath)).map((item) => ({
|
|
68
|
+
...item,
|
|
69
|
+
path: `${unitId}/${item.path}`,
|
|
70
|
+
})));
|
|
71
|
+
const safety = await analyzeUpdateSafety(oldPath, newPath);
|
|
72
|
+
findings.push(...safety.findings.map((finding) => ({
|
|
73
|
+
...finding,
|
|
74
|
+
paths: finding.paths.map((path) => `${unitId}/${path}`),
|
|
75
|
+
})));
|
|
76
|
+
}
|
|
77
|
+
const unique = new Map();
|
|
78
|
+
for (const finding of findings)
|
|
79
|
+
unique.set(`${finding.severity}:${finding.category}:${finding.message}:${finding.paths.join(",")}:${finding.names?.join(",") ?? ""}`, finding);
|
|
80
|
+
const safetyFindings = [...unique.values()];
|
|
81
|
+
return {
|
|
82
|
+
diff,
|
|
83
|
+
safetyFindings,
|
|
84
|
+
approvalRequired: safetyFindings.some((finding) => finding.severity === "blocking"),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
11
87
|
/** Builds a read-only update plan from persisted installs and live GitHub snapshots. */
|
|
12
88
|
export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository)) {
|
|
13
89
|
const state = await readInstallState();
|
|
@@ -39,10 +115,10 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
39
115
|
let approvalRequired = false;
|
|
40
116
|
if (!same && current.path) {
|
|
41
117
|
const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
safetyFindings =
|
|
45
|
-
approvalRequired =
|
|
118
|
+
const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
|
|
119
|
+
diff = analysis.diff;
|
|
120
|
+
safetyFindings = analysis.safetyFindings;
|
|
121
|
+
approvalRequired = analysis.approvalRequired;
|
|
46
122
|
}
|
|
47
123
|
return {
|
|
48
124
|
...base,
|
|
@@ -148,18 +224,32 @@ export async function applyPackageUpdate(packageId, options = {}, runtime = {})
|
|
|
148
224
|
if (current.commit.toLowerCase() === record.resolvedCommit.toLowerCase())
|
|
149
225
|
throw new Error(`Package '${packageId}' is already up to date`);
|
|
150
226
|
const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
|
|
151
|
-
const
|
|
227
|
+
const units = managedUnitIds(state, packageId);
|
|
228
|
+
const safety = await analyzeManagedUpdate(oldPath, current.path, units);
|
|
152
229
|
if (safety.approvalRequired && !options.approveRisk) {
|
|
153
230
|
const quarantinePath = options.quarantineOnBlock === false
|
|
154
231
|
? undefined
|
|
155
|
-
: await quarantineUpdate(packageId, current.repository, current.commit, safety.
|
|
156
|
-
throw new Error(`Update is blocked pending explicit risk approval${quarantinePath ? `; quarantined at ${quarantinePath}` : ""}: ${safety.
|
|
232
|
+
: await quarantineUpdate(packageId, current.repository, current.commit, safety.safetyFindings);
|
|
233
|
+
throw new Error(`Update is blocked pending explicit risk approval${quarantinePath ? `; quarantined at ${quarantinePath}` : ""}: ${safety.safetyFindings
|
|
157
234
|
.filter((finding) => finding.severity === "blocking")
|
|
158
235
|
.map((finding) => finding.message)
|
|
159
236
|
.join(" ")}`);
|
|
160
237
|
}
|
|
161
238
|
const agents = installedAgents(await (runtime.detectAgents ?? detectAgents)(), record.targetAgents);
|
|
162
|
-
const plan = await (runtime.buildPlan ?? buildSkillPlan)(current.path, record.packageId, agents
|
|
239
|
+
const plan = await (runtime.buildPlan ?? buildSkillPlan)(current.path, record.packageId, agents, units.length
|
|
240
|
+
? {
|
|
241
|
+
include: (skill) => units.some((unitId) => unitId.toLowerCase() === skill.name?.toLowerCase() ||
|
|
242
|
+
unitId.toLowerCase() === skill.targetName.toLowerCase()),
|
|
243
|
+
}
|
|
244
|
+
: {});
|
|
245
|
+
if (units.length) {
|
|
246
|
+
const plannedUnits = [
|
|
247
|
+
...new Set(plan.files.map((file) => basename(file.target))),
|
|
248
|
+
].sort();
|
|
249
|
+
if (plannedUnits.length !== units.length ||
|
|
250
|
+
plannedUnits.some((unitId, index) => unitId.toLowerCase() !== units[index].toLowerCase()))
|
|
251
|
+
throw new Error(`Update plan changed the managed skill set for '${packageId}'; expected ${units.join(", ")}, received ${plannedUnits.join(", ") || "none"}.`);
|
|
252
|
+
}
|
|
163
253
|
const verifier = runtime.verify ?? verifyInstalledSkills;
|
|
164
254
|
let verificationFailure;
|
|
165
255
|
let verificationSnapshotId;
|
|
@@ -167,6 +257,17 @@ export async function applyPackageUpdate(packageId, options = {}, runtime = {})
|
|
|
167
257
|
const snapshotId = await applySkillInstall(plan, {
|
|
168
258
|
repository: current.repository,
|
|
169
259
|
resolvedCommit: current.commit,
|
|
260
|
+
reviewed: true,
|
|
261
|
+
staticAssessment: {
|
|
262
|
+
status: safety.approvalRequired
|
|
263
|
+
? "blocking"
|
|
264
|
+
: safety.safetyFindings.length
|
|
265
|
+
? "warning"
|
|
266
|
+
: "clear",
|
|
267
|
+
findingCount: safety.safetyFindings.length,
|
|
268
|
+
assessedAt: new Date().toISOString(),
|
|
269
|
+
policy: "install-safety-v1",
|
|
270
|
+
},
|
|
170
271
|
}, {
|
|
171
272
|
allowManagedReplacement: true,
|
|
172
273
|
replaceManagedTargets: true,
|
package/dist/src/core/upgrade.js
CHANGED
|
@@ -33,6 +33,7 @@ export async function planUpgrade(selection, options = {}) {
|
|
|
33
33
|
? { fetchSnapshot: options.fetchSnapshot }
|
|
34
34
|
: {}),
|
|
35
35
|
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
36
|
+
...(options.access ? { access: options.access } : {}),
|
|
36
37
|
}),
|
|
37
38
|
outcomes(),
|
|
38
39
|
]);
|
|
@@ -257,6 +257,14 @@ export const installRecordSchema = z
|
|
|
257
257
|
files: z.array(fileHashSchema),
|
|
258
258
|
snapshotId: text,
|
|
259
259
|
installedAt: text,
|
|
260
|
+
staticAssessment: z
|
|
261
|
+
.object({
|
|
262
|
+
status: z.enum(["clear", "warning", "blocking"]),
|
|
263
|
+
findingCount: z.number().int().nonnegative(),
|
|
264
|
+
assessedAt: z.iso.datetime(),
|
|
265
|
+
policy: text,
|
|
266
|
+
})
|
|
267
|
+
.optional(),
|
|
260
268
|
})
|
|
261
269
|
.passthrough();
|
|
262
270
|
export const mcpInstallRecordSchema = z
|