loadout-ai 0.3.1 → 0.4.1
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 +67 -0
- package/MASTER_PLAN.md +177 -13
- package/README.md +157 -284
- package/dashboard/app.js +4 -4
- package/dashboard/index.html +4 -4
- package/dist/src/cli.js +27 -21
- package/dist/src/core/active-policy.js +172 -38
- package/dist/src/core/active-set.js +13 -4
- 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/conformance.js +74 -0
- package/dist/src/core/install.js +11 -11
- 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 +104 -12
- package/dist/src/core/runtime-tools.js +5 -2
- package/dist/src/core/scheduler.js +2 -1
- package/dist/src/core/snapshot.js +58 -13
- package/dist/src/core/state.js +8 -1
- package/dist/src/core/target-occupancy.js +50 -0
- package/dist/src/core/transaction.js +2 -1
- package/dist/src/core/uninstall.js +26 -2
- package/dist/src/dashboard.js +5 -2
- package/dist/src/shared/schemas.js +57 -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 +50 -0
- package/docs/USER_TEST_GUIDE.md +42 -4
- 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/plans/2026-07-20-project-activation-safety.md +469 -0
- package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +80 -0
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +228 -0
- package/package.json +8 -4
- package/SIMPLE_PLAN.md +0 -44
- package/docs/plans/2026-07-18-release-0.3.md +0 -42
- package/docs/superpowers/plans/2026-07-18-cli-ux-polish.md +0 -86
package/dist/src/core/adopt.js
CHANGED
|
@@ -1,10 +1,125 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, posix, relative, sep } from "node:path";
|
|
4
4
|
import { enrichInventoryWithProvenance } from "./provenance.js";
|
|
5
5
|
import { scanInstalledSkills } from "./skill-inventory.js";
|
|
6
6
|
import { installStatePath, recordInstall } from "./state.js";
|
|
7
7
|
import { runMutationTransaction } from "./transaction.js";
|
|
8
|
+
const issuedAdoptionPlans = new WeakMap();
|
|
9
|
+
function safeRelativePath(root, path) {
|
|
10
|
+
const value = relative(root, path).split(sep).join("/");
|
|
11
|
+
if (!value || isAbsolute(value) || value === ".." || value.startsWith("../"))
|
|
12
|
+
throw new Error(`Unsafe path while inspecting adoption tree: ${path}`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
async function captureAdoptionTree(root) {
|
|
16
|
+
const rootInfo = await lstat(root);
|
|
17
|
+
if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory())
|
|
18
|
+
throw new Error(`Refusing unsafe adoption root: ${root}`);
|
|
19
|
+
const entries = [];
|
|
20
|
+
async function visit(directory) {
|
|
21
|
+
const children = (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
|
|
22
|
+
for (const child of children) {
|
|
23
|
+
const path = join(directory, child.name);
|
|
24
|
+
const relativePath = safeRelativePath(root, path);
|
|
25
|
+
const info = await lstat(path);
|
|
26
|
+
if (info.isSymbolicLink())
|
|
27
|
+
throw new Error(`Refusing symlink while inspecting adoption tree: ${path}`);
|
|
28
|
+
if (info.isDirectory()) {
|
|
29
|
+
entries.push({ path: relativePath, type: "directory" });
|
|
30
|
+
await visit(path);
|
|
31
|
+
}
|
|
32
|
+
else if (info.isFile()) {
|
|
33
|
+
entries.push({
|
|
34
|
+
path: relativePath,
|
|
35
|
+
type: "file",
|
|
36
|
+
sha256: createHash("sha256")
|
|
37
|
+
.update(await readFile(path))
|
|
38
|
+
.digest("hex"),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
throw new Error(`Refusing special file while inspecting adoption tree: ${path}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await visit(root);
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
function validateTreeEvidence(entries) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.path ||
|
|
53
|
+
isAbsolute(entry.path) ||
|
|
54
|
+
entry.path === ".." ||
|
|
55
|
+
entry.path.startsWith("../") ||
|
|
56
|
+
entry.path.includes("\\") ||
|
|
57
|
+
entry.path.split("/").includes("..") ||
|
|
58
|
+
posix.normalize(entry.path) !== entry.path ||
|
|
59
|
+
(entry.type === "file" && !/^[a-f0-9]{64}$/.test(entry.sha256 ?? "")) ||
|
|
60
|
+
(entry.type === "directory" && entry.sha256 !== undefined) ||
|
|
61
|
+
(entry.type !== "file" && entry.type !== "directory") ||
|
|
62
|
+
seen.has(entry.path))
|
|
63
|
+
throw new Error("The adoption preview contains unsafe tree evidence; preview again.");
|
|
64
|
+
seen.add(entry.path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function adoptionWarnings(exact, reviewed) {
|
|
68
|
+
if (reviewed)
|
|
69
|
+
return [];
|
|
70
|
+
if (exact)
|
|
71
|
+
return [
|
|
72
|
+
"SKILL.md matches the reviewed catalog, but auxiliary local entries are not covered by catalog evidence; adoption remains unreviewed.",
|
|
73
|
+
];
|
|
74
|
+
return [
|
|
75
|
+
"The installed bytes do not exactly match the reviewed catalog; adoption records ownership but does not mark them reviewed.",
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function derivedReviewState(plan) {
|
|
79
|
+
const exact = plan.provenance.kind === "catalog-exact";
|
|
80
|
+
const tree = plan.treeEvidence ?? [];
|
|
81
|
+
return {
|
|
82
|
+
exact,
|
|
83
|
+
reviewed: exact &&
|
|
84
|
+
tree.length === 1 &&
|
|
85
|
+
tree[0].type === "file" &&
|
|
86
|
+
tree[0].path === "SKILL.md",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function deepFreeze(value) {
|
|
90
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
91
|
+
for (const child of Object.values(value))
|
|
92
|
+
deepFreeze(child);
|
|
93
|
+
Object.freeze(value);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function canonicalInstallPlan(packageId, agent, name, path, warnings) {
|
|
98
|
+
return {
|
|
99
|
+
packageId,
|
|
100
|
+
targetAgents: [agent.id],
|
|
101
|
+
warnings,
|
|
102
|
+
files: [
|
|
103
|
+
{
|
|
104
|
+
source: path,
|
|
105
|
+
target: path,
|
|
106
|
+
targetAgent: agent.id,
|
|
107
|
+
componentType: "skill",
|
|
108
|
+
compatibility: "native",
|
|
109
|
+
skillName: name,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function expectedRecordedFiles(plan) {
|
|
115
|
+
return plan.treeEvidence
|
|
116
|
+
.filter((entry) => entry.type === "file" && Boolean(entry.sha256))
|
|
117
|
+
.map((entry) => ({
|
|
118
|
+
path: join(plan.path, entry.path),
|
|
119
|
+
sha256: entry.sha256,
|
|
120
|
+
}))
|
|
121
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
122
|
+
}
|
|
8
123
|
function slug(value) {
|
|
9
124
|
return (value
|
|
10
125
|
.toLowerCase()
|
|
@@ -29,64 +144,82 @@ export async function planSkillAdoption(skill, agent, index) {
|
|
|
29
144
|
? match.provenance.candidates[0]
|
|
30
145
|
: undefined;
|
|
31
146
|
const packageId = `adopted-${agent.id}-${slug(match.name)}`;
|
|
32
|
-
|
|
147
|
+
const treeEvidence = await captureAdoptionTree(match.path);
|
|
148
|
+
const reviewed = Boolean(exact) &&
|
|
149
|
+
treeEvidence.length === 1 &&
|
|
150
|
+
treeEvidence[0].type === "file" &&
|
|
151
|
+
treeEvidence[0].path === "SKILL.md";
|
|
152
|
+
const warnings = adoptionWarnings(Boolean(exact), reviewed);
|
|
153
|
+
const plan = {
|
|
33
154
|
packageId,
|
|
34
|
-
agent,
|
|
155
|
+
agent: { ...agent },
|
|
35
156
|
name: match.name,
|
|
36
157
|
path: match.path,
|
|
37
158
|
fingerprint: match.fingerprint,
|
|
38
|
-
|
|
39
|
-
|
|
159
|
+
treeEvidence,
|
|
160
|
+
provenance: {
|
|
161
|
+
...match.provenance,
|
|
162
|
+
evidence: [...match.provenance.evidence],
|
|
163
|
+
candidates: match.provenance.candidates.map((candidate) => ({
|
|
164
|
+
...candidate,
|
|
165
|
+
})),
|
|
166
|
+
},
|
|
167
|
+
reviewed,
|
|
40
168
|
...(exact
|
|
41
169
|
? { repository: exact.repository, resolvedCommit: exact.commit }
|
|
42
170
|
: {}),
|
|
43
|
-
installPlan:
|
|
44
|
-
packageId,
|
|
45
|
-
targetAgents: [agent.id],
|
|
46
|
-
warnings: exact
|
|
47
|
-
? []
|
|
48
|
-
: [
|
|
49
|
-
"The installed bytes do not exactly match the reviewed catalog; adoption records ownership but does not mark them reviewed.",
|
|
50
|
-
],
|
|
51
|
-
files: [
|
|
52
|
-
{
|
|
53
|
-
source: match.path,
|
|
54
|
-
target: match.path,
|
|
55
|
-
targetAgent: agent.id,
|
|
56
|
-
componentType: "skill",
|
|
57
|
-
compatibility: "native",
|
|
58
|
-
skillName: match.name,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
},
|
|
171
|
+
installPlan: canonicalInstallPlan(packageId, agent, match.name, match.path, warnings),
|
|
62
172
|
};
|
|
173
|
+
deepFreeze(plan);
|
|
174
|
+
issuedAdoptionPlans.set(plan, deepFreeze({
|
|
175
|
+
packageId: plan.packageId,
|
|
176
|
+
path: plan.path,
|
|
177
|
+
treeEvidence: plan.treeEvidence,
|
|
178
|
+
installPlan: plan.installPlan,
|
|
179
|
+
...(plan.repository ? { repository: plan.repository } : {}),
|
|
180
|
+
...(plan.resolvedCommit ? { resolvedCommit: plan.resolvedCommit } : {}),
|
|
181
|
+
reviewed: derivedReviewState(plan).reviewed,
|
|
182
|
+
}));
|
|
183
|
+
return plan;
|
|
63
184
|
}
|
|
64
|
-
export async function applySkillAdoption(plan) {
|
|
185
|
+
export async function applySkillAdoption(plan, options = {}) {
|
|
186
|
+
const issued = issuedAdoptionPlans.get(plan);
|
|
187
|
+
if (!issued)
|
|
188
|
+
throw new Error("The adoption plan was not issued by this planner process; preview again.");
|
|
65
189
|
const applied = await runMutationTransaction(async () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (current !== plan.fingerprint)
|
|
190
|
+
validateTreeEvidence(issued.treeEvidence);
|
|
191
|
+
const current = await captureAdoptionTree(issued.path);
|
|
192
|
+
if (JSON.stringify(current) !== JSON.stringify(issued.treeEvidence))
|
|
70
193
|
throw new Error("The skill changed after preview; scan again before adopting it.");
|
|
71
|
-
return { targets: [installStatePath()], value:
|
|
194
|
+
return { targets: [installStatePath()], value: issued };
|
|
72
195
|
}, async (freshPlan, snapshot) => {
|
|
196
|
+
await options.beforeRecord?.();
|
|
73
197
|
await recordInstall(freshPlan.installPlan, snapshot.id, {
|
|
74
198
|
...(freshPlan.repository ? { repository: freshPlan.repository } : {}),
|
|
75
199
|
...(freshPlan.resolvedCommit
|
|
76
200
|
? { resolvedCommit: freshPlan.resolvedCommit }
|
|
77
201
|
: {}),
|
|
78
202
|
reviewed: freshPlan.reviewed,
|
|
203
|
+
}, {
|
|
204
|
+
expectedFiles: expectedRecordedFiles(freshPlan),
|
|
205
|
+
verifyBeforeWrite: async () => {
|
|
206
|
+
const current = await captureAdoptionTree(freshPlan.path);
|
|
207
|
+
if (JSON.stringify(current) !== JSON.stringify(freshPlan.treeEvidence))
|
|
208
|
+
throw new Error("The skill changed after preview; scan again before adopting it.");
|
|
209
|
+
},
|
|
79
210
|
});
|
|
80
211
|
});
|
|
81
212
|
return applied.snapshotId;
|
|
82
213
|
}
|
|
83
214
|
export function formatAdoptionPlan(plan) {
|
|
215
|
+
const review = derivedReviewState(plan);
|
|
84
216
|
return [
|
|
85
217
|
`Adopt: ${plan.name} for ${plan.agent.displayName}`,
|
|
86
218
|
`Path: ${plan.path}`,
|
|
87
219
|
`Managed id: ${plan.packageId}`,
|
|
88
220
|
`Provenance: ${plan.provenance.kind} (${plan.provenance.confidence})`,
|
|
89
|
-
`Review state: ${
|
|
221
|
+
`Review state: ${review.reviewed ? "reviewed exact catalog match" : "unreviewed"}`,
|
|
222
|
+
`Bound tree entries: ${plan.treeEvidence?.length ?? 0}`,
|
|
90
223
|
...plan.installPlan.warnings.map((warning) => `Warning: ${warning}`),
|
|
91
224
|
].join("\n");
|
|
92
225
|
}
|
|
@@ -248,7 +248,7 @@ function activeSetCapacity(evidence) {
|
|
|
248
248
|
`Disable or consolidate at least ${active - capacity} active skill(s), prioritizing duplicates and weak evidence.`,
|
|
249
249
|
]
|
|
250
250
|
: [
|
|
251
|
-
"Keep nonessential packages in the
|
|
251
|
+
"Keep nonessential packages in the inspected library and re-check capacity after activation changes.",
|
|
252
252
|
]);
|
|
253
253
|
}
|
|
254
254
|
function compatibility(evidence) {
|
|
@@ -464,7 +464,7 @@ export function buildAgentHealthScore(evidence) {
|
|
|
464
464
|
}
|
|
465
465
|
export function formatAgentHealthScore(score) {
|
|
466
466
|
const lines = [
|
|
467
|
-
`
|
|
467
|
+
`Evidence coverage and managed-state hygiene: ${score.score}/${score.maximumScore} (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`,
|
|
468
468
|
`Agent: ${score.agent}${score.asOf ? ` · observed ${score.asOf}` : ""}`,
|
|
469
469
|
];
|
|
470
470
|
for (const item of score.dimensions) {
|
|
@@ -42,6 +42,7 @@ export function buildCatalogCoverage(catalog, targetRecords = 50) {
|
|
|
42
42
|
const platforms = tally(catalog.flatMap((pkg) => pkg.operatingSystems ?? []), operatingSystems);
|
|
43
43
|
return {
|
|
44
44
|
records: catalog.length,
|
|
45
|
+
categoryCount: Object.keys(categories).length,
|
|
45
46
|
targetRecords,
|
|
46
47
|
technicallyScreenedRecords: catalog.filter(technicallyScreened).length,
|
|
47
48
|
recommendedRecords: catalog.filter((pkg) => catalogTrustStage(pkg) === "recommended").length,
|
|
@@ -76,7 +77,7 @@ export function formatCatalogCoverage(report) {
|
|
|
76
77
|
.join(", ");
|
|
77
78
|
return [
|
|
78
79
|
`Screened catalog: ${report.technicallyScreenedRecords}/${report.records} technically complete records (target ${report.targetRecords})`,
|
|
79
|
-
`
|
|
80
|
+
`Policy selection: ${report.recommendedRecords} Stable sources · ${report.trustStages["human-reviewed"]} human-reviewed · ${report.trustStages.benchmarked} benchmarked`,
|
|
80
81
|
`Evidence: ${report.immutablePins} immutable pins · ${report.assertedLicenses} asserted licenses · ${report.noAssertionLicenses} NOASSERTION`,
|
|
81
82
|
`Coverage: ${Object.values(report.categories).length} categories · ${report.evaluationReady} evaluation-ready · ${report.activityObserved} with refreshed activity`,
|
|
82
83
|
`Install shape: ${report.installShapes.skills} skill · ${report.installShapes.mcpOnly} MCP-only · ${report.installShapes.mixed} mixed`,
|
|
@@ -5,6 +5,7 @@ import { applySkillLibraryBatch, applySkillInstallBatch, buildSkillPlan, install
|
|
|
5
5
|
import { fetchRepositorySnapshot, } from "./source.js";
|
|
6
6
|
import { analyzeInstallPlanSafety, } from "./safety.js";
|
|
7
7
|
import { formatModelApiAccess } from "./access.js";
|
|
8
|
+
import { recordInstalledProfile } from "./profile-state.js";
|
|
8
9
|
export const RECOMMENDED_ACTIVE_SKILL_LIMIT = 30;
|
|
9
10
|
async function parallelMap(values, concurrency, worker) {
|
|
10
11
|
const results = new Array(values.length);
|
|
@@ -244,12 +245,18 @@ export async function applyPreparedCatalogInstall(prepared, options = {}) {
|
|
|
244
245
|
if (risky.length && !options.approveRisk)
|
|
245
246
|
throw new Error(`Additional risk approval is required for: ${risky.map((entry) => entry.package.id).join(", ")}. Review the plan, then use --approve-risk.`);
|
|
246
247
|
return prepared.selection.mode === "maximum"
|
|
247
|
-
? applySkillLibraryBatch(prepared.entries
|
|
248
|
+
? applySkillLibraryBatch(prepared.entries, {
|
|
249
|
+
afterRecord: () => recordInstalledProfile(prepared).then(() => undefined),
|
|
250
|
+
})
|
|
248
251
|
: applySkillInstallBatch(prepared.entries, [], {
|
|
249
252
|
replaceManagedTargets: true,
|
|
250
253
|
reconcileManagedTargets: true,
|
|
251
254
|
...(prepared.reconciliation
|
|
252
255
|
? { expectedReconciliation: prepared.reconciliation }
|
|
253
256
|
: {}),
|
|
257
|
+
afterRecord: () => recordInstalledProfile(prepared).then(() => undefined),
|
|
254
258
|
});
|
|
255
259
|
}
|
|
260
|
+
export function formatCatalogApplyGuidance(riskApprovalRequired) {
|
|
261
|
+
return `Preview complete; nothing was changed. Re-run with --yes${riskApprovalRequired ? " --approve-risk" : ""} to install this exact screened plan.`;
|
|
262
|
+
}
|
|
@@ -7,7 +7,7 @@ import { catalogTrustPath, loadEffectiveCatalog, readCatalogTrustState, readTrus
|
|
|
7
7
|
import { ensureDirectory } from "./paths.js";
|
|
8
8
|
import { loadoutHome } from "./paths.js";
|
|
9
9
|
import { withFileLock } from "./file-lock.js";
|
|
10
|
-
import { createSnapshot } from "./snapshot.js";
|
|
10
|
+
import { createSnapshot, recordSnapshotPostMutationState } from "./snapshot.js";
|
|
11
11
|
import { verifyEnvelope } from "./signing.js";
|
|
12
12
|
import { beginTransaction, completeTransaction, markTransactionCommitting, rollbackTransaction, recoverPendingTransactions, } from "./transaction.js";
|
|
13
13
|
const MAX_RELEASE_BYTES = 5 * 1024 * 1024;
|
|
@@ -248,6 +248,7 @@ export async function applyCatalogRelease(preview, options = {}) {
|
|
|
248
248
|
}, null, 2)}\n`);
|
|
249
249
|
await markTransactionCommitting(transaction);
|
|
250
250
|
await writeFileAtomically(target, `${JSON.stringify(state, null, 2)}\n`);
|
|
251
|
+
await recordSnapshotPostMutationState(snapshot);
|
|
251
252
|
await completeTransaction(transaction, { releaseLock: false });
|
|
252
253
|
await writeFileAtomically(catalogTrustPath(), `${JSON.stringify({
|
|
253
254
|
...pinned,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ADAPTER_CAPABILITIES, agentComponentDirectory } from "./adapters.js";
|
|
2
|
+
import { AGENT_DEFINITIONS, agentSkillsDirectory } from "./paths.js";
|
|
3
|
+
const PLATFORM_BY_RUNNER = {
|
|
4
|
+
"ubuntu-latest": "linux",
|
|
5
|
+
"macos-latest": "macos",
|
|
6
|
+
"windows-latest": "windows",
|
|
7
|
+
};
|
|
8
|
+
/** Derive bounded platform evidence from the manually triggered CI job. */
|
|
9
|
+
export function platformEvidenceFromCiWorkflow(workflow) {
|
|
10
|
+
const jobStart = workflow.search(/^ {2}cross-platform:\s*$/m);
|
|
11
|
+
const dispatchConfigured = /^ {2}workflow_dispatch:\s*$/m.test(workflow);
|
|
12
|
+
if (jobStart < 0 || !dispatchConfigured)
|
|
13
|
+
throw new Error("The cross-platform CI job and workflow_dispatch trigger are required before platform evidence can be claimed.");
|
|
14
|
+
const afterStart = workflow.slice(jobStart + 1);
|
|
15
|
+
const nextJob = afterStart.search(/^ {2}[a-zA-Z0-9_-]+:\s*$/m);
|
|
16
|
+
const job = nextJob < 0 ? afterStart : afterStart.slice(0, nextJob);
|
|
17
|
+
if (!/if:\s*github\.event_name\s*==\s*['"]workflow_dispatch['"]/.test(job))
|
|
18
|
+
throw new Error("The cross-platform CI job must remain explicitly bounded to workflow_dispatch.");
|
|
19
|
+
const match = job.match(/^\s+os:\s*\[([^\]]+)\]\s*$/m);
|
|
20
|
+
if (!match)
|
|
21
|
+
throw new Error("The cross-platform CI job has no explicit OS matrix.");
|
|
22
|
+
const runners = match[1].split(",").map((value) => value.trim());
|
|
23
|
+
return runners.map((runner) => {
|
|
24
|
+
const platform = PLATFORM_BY_RUNNER[runner];
|
|
25
|
+
if (!platform)
|
|
26
|
+
throw new Error(`The cross-platform CI job uses an unrecognized runner '${runner}'.`);
|
|
27
|
+
return {
|
|
28
|
+
platform,
|
|
29
|
+
kind: "ci-configured",
|
|
30
|
+
source: ".github/workflows/ci.yml (cross-platform job)",
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function declaredAgents() {
|
|
35
|
+
return AGENT_DEFINITIONS.map((definition) => ({
|
|
36
|
+
id: definition.id,
|
|
37
|
+
displayName: definition.displayName,
|
|
38
|
+
installed: false,
|
|
39
|
+
skillsDirectory: agentSkillsDirectory(definition.id),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build conservative adapter evidence. Merely constructing this matrix does
|
|
44
|
+
* not prove a filesystem run or execution inside an agent application.
|
|
45
|
+
*/
|
|
46
|
+
export function buildAdapterConformanceMatrix(agents = declaredAgents(), platformEvidence = []) {
|
|
47
|
+
const detected = new Map(agents.map((agent) => [agent.id, agent]));
|
|
48
|
+
return ADAPTER_CAPABILITIES.map((adapter) => {
|
|
49
|
+
const agent = detected.get(adapter.agent);
|
|
50
|
+
return {
|
|
51
|
+
agent: adapter.agent,
|
|
52
|
+
displayName: adapter.displayName,
|
|
53
|
+
pathKnown: Boolean(agent) &&
|
|
54
|
+
Boolean(agentComponentDirectory(agent, "skill")) &&
|
|
55
|
+
adapter.components.skill === "native",
|
|
56
|
+
filesystemVerified: false,
|
|
57
|
+
nativeApplicationVerified: false,
|
|
58
|
+
platformEvidence: platformEvidence.map((item) => ({
|
|
59
|
+
...item,
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Promote only the filesystem evidence after the caller has completed the
|
|
66
|
+
* disposable plan/apply/inspect/disable/enable/rollback lifecycle.
|
|
67
|
+
*/
|
|
68
|
+
export function markFilesystemConformanceVerified(matrix, agent) {
|
|
69
|
+
if (!matrix.some((entry) => entry.agent === agent && entry.pathKnown))
|
|
70
|
+
throw new Error(`Cannot verify filesystem conformance for '${agent}' without a known native skill path.`);
|
|
71
|
+
return matrix.map((entry) => entry.agent === agent
|
|
72
|
+
? { ...entry, filesystemVerified: true }
|
|
73
|
+
: { ...entry });
|
|
74
|
+
}
|
package/dist/src/core/install.js
CHANGED
|
@@ -6,6 +6,7 @@ import { planAdapterSkillInstall } from "./adapters.js";
|
|
|
6
6
|
import { applySkillPlan, detectInstallConflicts } from "./skills.js";
|
|
7
7
|
import { activationLibraryPath, installStatePath, recordInstall, recordInstallBatch, recordLibraryInstallBatch, readInstallState, hashDirectory, writeInstallState, } from "./state.js";
|
|
8
8
|
import { runMutationTransaction } from "./transaction.js";
|
|
9
|
+
import { inspectTargetOccupancy } from "./target-occupancy.js";
|
|
9
10
|
export function installedAgents(agents, requested) {
|
|
10
11
|
const available = agents.filter((agent) => agent.installed);
|
|
11
12
|
if (!requested || requested.length === 0)
|
|
@@ -116,17 +117,8 @@ async function assertActiveTargetsUnoccupied(plans, options = {}) {
|
|
|
116
117
|
for (const target of [
|
|
117
118
|
...new Set(plans.flatMap((plan) => plan.files.map((file) => file.target))),
|
|
118
119
|
])
|
|
119
|
-
|
|
120
|
-
await lstat(target);
|
|
120
|
+
if ((await inspectTargetOccupancy(target)).occupied)
|
|
121
121
|
occupied.push(target);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
if (!error ||
|
|
125
|
-
typeof error !== "object" ||
|
|
126
|
-
!("code" in error) ||
|
|
127
|
-
error.code !== "ENOENT")
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
122
|
if (occupied.length && options.allowManagedReplacement) {
|
|
131
123
|
const state = await readInstallState();
|
|
132
124
|
const allowed = new Map();
|
|
@@ -202,6 +194,9 @@ export async function applySkillInstall(plan, metadata, options = {}) {
|
|
|
202
194
|
value: plan,
|
|
203
195
|
};
|
|
204
196
|
}, async (freshPlan, snapshot) => {
|
|
197
|
+
// Close the preview/apply race: an empty target may have become occupied
|
|
198
|
+
// after the first check but before the transaction snapshot completed.
|
|
199
|
+
await assertActiveTargetsUnoccupied([freshPlan], options);
|
|
205
200
|
if (options.replaceManagedTargets)
|
|
206
201
|
for (const target of [
|
|
207
202
|
...new Set(freshPlan.files.map((file) => file.target)),
|
|
@@ -264,6 +259,9 @@ export async function applySkillInstallBatch(entries, extraSnapshotPaths = [], o
|
|
|
264
259
|
value: { entries, reconciliation },
|
|
265
260
|
};
|
|
266
261
|
}, async ({ entries: freshEntries, reconciliation }, snapshot) => {
|
|
262
|
+
// Re-check immediately before any removal or copy for the same reason as
|
|
263
|
+
// the single-package path above.
|
|
264
|
+
await assertActiveTargetsUnoccupied(freshEntries.map((entry) => entry.plan), { allowManagedReplacement: options.replaceManagedTargets });
|
|
267
265
|
for (const target of reconciliation.obsoleteTargets)
|
|
268
266
|
await rm(target, { recursive: true, force: true });
|
|
269
267
|
if (options.replaceManagedTargets)
|
|
@@ -279,6 +277,7 @@ export async function applySkillInstallBatch(entries, extraSnapshotPaths = [], o
|
|
|
279
277
|
await assertExactDirectoryCopy(file.source, file.target, "Exact setup copy verification failed");
|
|
280
278
|
await recordInstallBatch(freshEntries, snapshot.id);
|
|
281
279
|
await recordManagedProfileReconciliation(reconciliation);
|
|
280
|
+
await options.afterRecord?.();
|
|
282
281
|
});
|
|
283
282
|
return applied.snapshotId;
|
|
284
283
|
}
|
|
@@ -286,7 +285,7 @@ export async function applySkillInstallBatch(entries, extraSnapshotPaths = [], o
|
|
|
286
285
|
* Download a batch into the reviewed library without exposing any skill to an
|
|
287
286
|
* agent yet. This is the safe destination for Maximum Library.
|
|
288
287
|
*/
|
|
289
|
-
export async function applySkillLibraryBatch(entries) {
|
|
288
|
+
export async function applySkillLibraryBatch(entries, options = {}) {
|
|
290
289
|
if (!entries.length)
|
|
291
290
|
throw new Error("Library batch is empty");
|
|
292
291
|
const conflicts = detectInstallConflicts(entries.map((entry) => entry.plan));
|
|
@@ -343,6 +342,7 @@ export async function applySkillLibraryBatch(entries) {
|
|
|
343
342
|
}
|
|
344
343
|
}
|
|
345
344
|
await recordLibraryInstallBatch(freshEntries, snapshot.id);
|
|
345
|
+
await options.afterRecord?.();
|
|
346
346
|
});
|
|
347
347
|
return applied.snapshotId;
|
|
348
348
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compareCatalogPackages } from "./ranking.js";
|
|
2
2
|
/**
|
|
3
|
-
* Stable is Loadout's
|
|
3
|
+
* Stable is Loadout's bounded policy-selected daily driver: broad enough to improve normal
|
|
4
4
|
* engineering work immediately, but bounded at skill granularity and restricted
|
|
5
5
|
* to catalog sources with an identified SPDX license. Maximum remains the
|
|
6
6
|
* explicit broad-library mode.
|
|
@@ -45,7 +45,8 @@ export const STABLE_BOOST_PACKAGE_IDS = Object.freeze(Object.keys(STABLE_SKILL_A
|
|
|
45
45
|
/**
|
|
46
46
|
* Trust is deliberately separate from popularity and publisher tier. No
|
|
47
47
|
* bundled record is labelled human-reviewed or benchmarked until that evidence
|
|
48
|
-
* is actually stored; Stable is the policy-
|
|
48
|
+
* is actually stored; Stable is the policy-selected subset. The stored
|
|
49
|
+
* `recommended` value remains for schema compatibility.
|
|
49
50
|
*/
|
|
50
51
|
export function catalogTrustStage(pkg) {
|
|
51
52
|
if (STABLE_BOOST_PACKAGE_IDS.includes(pkg.id) &&
|
|
@@ -58,6 +59,10 @@ export function catalogTrustStage(pkg) {
|
|
|
58
59
|
return "inspected";
|
|
59
60
|
return "discovered";
|
|
60
61
|
}
|
|
62
|
+
/** Keep stored `recommended` values compatible while naming their evidence honestly. */
|
|
63
|
+
export function formatCatalogTrustStage(stage) {
|
|
64
|
+
return stage === "recommended" ? "policy-selected" : stage;
|
|
65
|
+
}
|
|
61
66
|
export function isStableSkillSelected(packageId, skillName, targetName) {
|
|
62
67
|
const selected = STABLE_SKILL_ALLOWLIST[packageId];
|
|
63
68
|
if (!selected)
|
|
@@ -233,7 +238,7 @@ export function resolveCatalogProfile(packages, selection, families = CATALOG_CO
|
|
|
233
238
|
selected.delete(secondary.id);
|
|
234
239
|
deferred.push(secondary);
|
|
235
240
|
}
|
|
236
|
-
warnings.push(`Stable
|
|
241
|
+
warnings.push(`Loadout policy selection for Stable chose ${primary.displayName}; deferred ${members
|
|
237
242
|
.slice(1)
|
|
238
243
|
.map((pkg) => pkg.displayName)
|
|
239
244
|
.join(", ")} because they overlap in ${family.label}.`);
|
|
@@ -242,7 +247,7 @@ export function resolveCatalogProfile(packages, selection, families = CATALOG_CO
|
|
|
242
247
|
warnings.push(`Custom selection retains the soft overlap in ${family.label}. Review ${names} before installation.`);
|
|
243
248
|
}
|
|
244
249
|
else {
|
|
245
|
-
warnings.push(`${selection.mode === "power" ? "Power Boost" : "Maximum Library"} retains the soft overlap in ${family.label}. ${primary.displayName}
|
|
250
|
+
warnings.push(`${selection.mode === "power" ? "Power Boost" : "Maximum Library"} retains the soft overlap in ${family.label}. Loadout policy orders ${primary.displayName} first; review each package before installation.`);
|
|
246
251
|
}
|
|
247
252
|
}
|
|
248
253
|
return {
|
package/dist/src/core/ranking.js
CHANGED
|
@@ -96,7 +96,7 @@ export function explainCatalogScore(pkg, now = new Date()) {
|
|
|
96
96
|
],
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
/** A deterministic ordering:
|
|
99
|
+
/** A deterministic policy ordering: declared tier, bounded evidence score, then id. */
|
|
100
100
|
export function compareCatalogPackages(a, b) {
|
|
101
101
|
return (TIER_ORDER[b.tier] - TIER_ORDER[a.tier] ||
|
|
102
102
|
explainCatalogScore(b).score - explainCatalogScore(a).score ||
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { formatSchemaError, readmeClaimManifestSchema, } from "../shared/schemas.js";
|
|
2
|
+
export { readmeClaimManifestSchema };
|
|
3
|
+
/** Parse the versioned index of evidence for material README statements. */
|
|
4
|
+
export function parseReadmeClaimManifest(value) {
|
|
5
|
+
const result = readmeClaimManifestSchema.safeParse(value);
|
|
6
|
+
if (!result.success) {
|
|
7
|
+
throw new Error(`README claim manifest is invalid: ${formatSchemaError(result.error)}`);
|
|
8
|
+
}
|
|
9
|
+
return result.data;
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { supportedAdapterNames } from "./adapters.js";
|
|
2
|
+
import { buildCatalogCoverage } from "./catalog-coverage.js";
|
|
3
|
+
function profileFacts(allowlist) {
|
|
4
|
+
return {
|
|
5
|
+
sources: Object.keys(allowlist).length,
|
|
6
|
+
skillDirectories: Object.values(allowlist).reduce((count, skills) => count + skills.length, 0),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Derive all changeable README facts from passed authoritative source data.
|
|
11
|
+
* This module intentionally neither reads nor parses README text.
|
|
12
|
+
*/
|
|
13
|
+
export function deriveReadmeFacts({ catalog, packageJson, agents, profiles, }) {
|
|
14
|
+
const coverage = buildCatalogCoverage(catalog);
|
|
15
|
+
return {
|
|
16
|
+
catalog: {
|
|
17
|
+
records: coverage.records,
|
|
18
|
+
categories: coverage.categoryCount,
|
|
19
|
+
components: coverage.components,
|
|
20
|
+
installShapes: coverage.installShapes,
|
|
21
|
+
assertedLicenses: coverage.assertedLicenses,
|
|
22
|
+
noAssertionLicenses: coverage.noAssertionLicenses,
|
|
23
|
+
},
|
|
24
|
+
profiles: {
|
|
25
|
+
stable: profileFacts(profiles.stable),
|
|
26
|
+
power: profileFacts(profiles.power),
|
|
27
|
+
},
|
|
28
|
+
agents: {
|
|
29
|
+
supportedNames: supportedAdapterNames(agents),
|
|
30
|
+
},
|
|
31
|
+
package: {
|
|
32
|
+
name: packageJson.name,
|
|
33
|
+
version: packageJson.version,
|
|
34
|
+
bin: { ...packageJson.bin },
|
|
35
|
+
},
|
|
36
|
+
runtime: {
|
|
37
|
+
node: packageJson.engines.node,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|