loadout-ai 0.3.1 → 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 +42 -0
- package/MASTER_PLAN.md +86 -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/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 +36 -3
- 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/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/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 +26 -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
- 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/cli.js
CHANGED
|
@@ -20,8 +20,8 @@ import { buildHealthReport, formatHealthReport } from "./core/health.js";
|
|
|
20
20
|
import { readInstallState, recordInstallTransaction } from "./core/state.js";
|
|
21
21
|
import { applyRemove, planRemove } from "./core/remove.js";
|
|
22
22
|
import { applyUninstall, buildUninstallPlan, formatUninstallPlan, uninstallGlobalCli, } from "./core/uninstall.js";
|
|
23
|
-
import { evaluateInstalledProfile, formatInstalledProfileStatus,
|
|
24
|
-
import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
|
|
23
|
+
import { evaluateInstalledProfile, formatInstalledProfileStatus, } from "./core/profile-state.js";
|
|
24
|
+
import { formatRecommendations, personalizeRecommendations, profileManifestPackages, recommendPackages, RECOMMENDATION_BOUNDARY, scanProject, TESTED_PROFILES, } from "./core/recommend.js";
|
|
25
25
|
import { buildImprovementCycle, formatImprovementCycle, recordImprovementOutcome, writeImprovementCycle, } from "./core/improve.js";
|
|
26
26
|
import { applySyncPlan, buildSyncPlan } from "./core/sync.js";
|
|
27
27
|
import { createPackage, packPackage, publishLocalPackage, publishRemotePackage, searchLocalRegistry, } from "./core/registry.js";
|
|
@@ -33,7 +33,7 @@ import { generateSigningKeys, signJsonFile, verifyJsonFile, } from "./core/signi
|
|
|
33
33
|
import { applyPortableImport, exportPortableLoadout, planPortableImport, } from "./core/portable.js";
|
|
34
34
|
import { applyCodexMcpConfigPlan, defaultCodexMcpConfigPath, planCodexMcpConfig, } from "./core/codex-mcp.js";
|
|
35
35
|
import { formatDemoResult, runIsolatedDemo } from "./core/demo.js";
|
|
36
|
-
import { catalogTrustStage, resolveCatalogProfile } from "./core/profiles.js";
|
|
36
|
+
import { catalogTrustStage, formatCatalogTrustStage, resolveCatalogProfile, } from "./core/profiles.js";
|
|
37
37
|
import { discoverHackerNewsRepositories } from "./core/community.js";
|
|
38
38
|
import { discoverPrivateRepositories } from "./core/private-discovery.js";
|
|
39
39
|
import { defaultGitHubDiscoveryQueries, discoverGitHubRepositories, } from "./core/github-discovery.js";
|
|
@@ -45,7 +45,7 @@ import { compileConversion, } from "./core/conversion.js";
|
|
|
45
45
|
import { writeFileAtomically } from "./core/atomic-file.js";
|
|
46
46
|
import { formatCanaryResult, runCanary } from "./core/canary.js";
|
|
47
47
|
import { startDashboardServer } from "./dashboard.js";
|
|
48
|
-
import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
48
|
+
import { applyPreparedCatalogInstall, formatCatalogApplyGuidance, formatPreparedCatalogInstall, prepareCatalogInstall, } from "./core/catalog-install.js";
|
|
49
49
|
import { formatInstalledSkillInventory, scanInstalledSkills, } from "./core/skill-inventory.js";
|
|
50
50
|
import { enrichInventoryWithProvenance, formatProvenanceSummary, resolveCatalogSkillIndex, } from "./core/provenance.js";
|
|
51
51
|
import { compareSkill, formatSkillComparison } from "./core/skill-compare.js";
|
|
@@ -183,14 +183,14 @@ async function runSetup(options) {
|
|
|
183
183
|
try {
|
|
184
184
|
if (!mode) {
|
|
185
185
|
if (!interactive) {
|
|
186
|
-
console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for
|
|
186
|
+
console.log("Loadout is CLI-first. Run `loadout setup --mode stable` for Loadout's bounded policy selection, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
189
|
reader = createInterface({
|
|
190
190
|
input: process.stdin,
|
|
191
191
|
output: process.stdout,
|
|
192
192
|
});
|
|
193
|
-
const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (
|
|
193
|
+
const answer = (await reader.question("Choose a loadout: [1] Stable Daily Driver (Loadout policy selection), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
|
|
194
194
|
mode =
|
|
195
195
|
answer === "2"
|
|
196
196
|
? "power"
|
|
@@ -228,7 +228,7 @@ async function runSetup(options) {
|
|
|
228
228
|
let riskApproved = Boolean(options.approveRisk);
|
|
229
229
|
if (!approved) {
|
|
230
230
|
if (!interactive) {
|
|
231
|
-
console.log(
|
|
231
|
+
console.log(formatCatalogApplyGuidance(Boolean(risky)));
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
reader ??= createInterface({
|
|
@@ -258,7 +258,6 @@ async function runSetup(options) {
|
|
|
258
258
|
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
259
259
|
approveRisk: riskApproved,
|
|
260
260
|
});
|
|
261
|
-
await recordInstalledProfile(prepared);
|
|
262
261
|
console.log(`\nLoadout installed ${prepared.entries.length} repositories for ${prepared.agents.length} agent(s). Snapshot: ${snapshotId}`);
|
|
263
262
|
console.log("Next: `loadout status`, `loadout optimize --project .`, or `loadout autopilot --yes` for opt-in daily read-only discovery and update checks.");
|
|
264
263
|
}
|
|
@@ -266,7 +265,7 @@ async function runSetup(options) {
|
|
|
266
265
|
reader?.close();
|
|
267
266
|
}
|
|
268
267
|
}
|
|
269
|
-
const LOADOUT_VERSION = "0.
|
|
268
|
+
const LOADOUT_VERSION = "0.4.0";
|
|
270
269
|
function durableSchedulerLauncher() {
|
|
271
270
|
return [
|
|
272
271
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
@@ -730,8 +729,8 @@ for (const workflow of ["activate", "optimize"]) {
|
|
|
730
729
|
program
|
|
731
730
|
.command(workflow)
|
|
732
731
|
.description(workflow === "activate"
|
|
733
|
-
? "Select and activate
|
|
734
|
-
: "Scan a project and propose
|
|
732
|
+
? "Select and activate inspected library skills for a project"
|
|
733
|
+
: "Scan a project and propose rule-selected inspected active-set additions")
|
|
735
734
|
.option("--project <path>", "project directory to scan", ".")
|
|
736
735
|
.option("--agents <ids>", "comma-separated agent ids")
|
|
737
736
|
.option("--limit <count>", "maximum active skills per agent", "40")
|
|
@@ -1055,6 +1054,7 @@ program
|
|
|
1055
1054
|
? JSON.stringify({
|
|
1056
1055
|
signals,
|
|
1057
1056
|
recommendations,
|
|
1057
|
+
recommendationBoundary: RECOMMENDATION_BOUNDARY,
|
|
1058
1058
|
personalization: options.agent
|
|
1059
1059
|
? {
|
|
1060
1060
|
agent: options.agent,
|
|
@@ -1066,7 +1066,7 @@ program
|
|
|
1066
1066
|
});
|
|
1067
1067
|
program
|
|
1068
1068
|
.command("profiles")
|
|
1069
|
-
.description("List
|
|
1069
|
+
.description("List Loadout policy profiles or inspect one profile")
|
|
1070
1070
|
.argument("[name]", "profile name")
|
|
1071
1071
|
.option("--json", "emit machine-readable JSON")
|
|
1072
1072
|
.option("--apply-to <path>", "add the selected profile packages to a manifest")
|
|
@@ -1183,7 +1183,7 @@ program
|
|
|
1183
1183
|
.description("Take ownership of one explicitly selected installed skill without changing its bytes")
|
|
1184
1184
|
.argument("<skill>", "installed skill name, directory name, or exact path")
|
|
1185
1185
|
.requiredOption("--agent <id>", "agent that owns the installed skill")
|
|
1186
|
-
.option("--refresh-provenance", "rebuild the
|
|
1186
|
+
.option("--refresh-provenance", "rebuild the inspected catalog skill index")
|
|
1187
1187
|
.option("--yes", "record ownership; otherwise show a dry-run plan")
|
|
1188
1188
|
.option("--json", "emit machine-readable JSON")
|
|
1189
1189
|
.action(async (skill, options) => {
|
|
@@ -1218,7 +1218,7 @@ program
|
|
|
1218
1218
|
.description("Compare an installed or reviewed skill with evidence-related catalog alternatives without changing anything")
|
|
1219
1219
|
.argument("<skill>", "installed skill name, directory name, or catalog package id")
|
|
1220
1220
|
.option("--agent <id>", "select one installed agent when names are ambiguous")
|
|
1221
|
-
.option("--refresh", "rebuild the
|
|
1221
|
+
.option("--refresh", "rebuild the inspected catalog skill index")
|
|
1222
1222
|
.option("--offline", "never fetch; use the existing local index only")
|
|
1223
1223
|
.option("--limit <count>", "maximum alternatives to show", "10")
|
|
1224
1224
|
.option("--json", "emit the complete machine-readable comparison")
|
|
@@ -1559,6 +1559,7 @@ program
|
|
|
1559
1559
|
category: pkg.category,
|
|
1560
1560
|
tier: pkg.tier,
|
|
1561
1561
|
trustStage: catalogTrustStage(pkg),
|
|
1562
|
+
evidenceLabel: formatCatalogTrustStage(catalogTrustStage(pkg)),
|
|
1562
1563
|
},
|
|
1563
1564
|
ranking: explainCatalogScore(pkg),
|
|
1564
1565
|
}, null, 2));
|
|
@@ -1580,7 +1581,7 @@ program
|
|
|
1580
1581
|
const updated = pkg.lastUpdatedAt
|
|
1581
1582
|
? ` — updated ${pkg.lastUpdatedAt.slice(0, 10)}`
|
|
1582
1583
|
: "";
|
|
1583
|
-
console.log(`${pkg.displayName} [${pkg.tier}; ${catalogTrustStage(pkg)}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
1584
|
+
console.log(`${pkg.displayName} [${pkg.tier}; ${formatCatalogTrustStage(catalogTrustStage(pkg))}] ★${pkg.stars ?? "?"} — ${pkg.repository}${topics}${updated}`);
|
|
1584
1585
|
}
|
|
1585
1586
|
for (const failure of result.failures)
|
|
1586
1587
|
console.error(`Warning: could not refresh ${failure.repository}: ${failure.error}`);
|
|
@@ -1648,7 +1649,7 @@ candidate
|
|
|
1648
1649
|
.description("Convert a reviewed dossier into a catalog-record proposal; never edits the catalog")
|
|
1649
1650
|
.argument("<dossier>", "persisted candidate dossier JSON")
|
|
1650
1651
|
.requiredOption("--id <id>", "lowercase kebab-case catalog id")
|
|
1651
|
-
.requiredOption("--category <category>", "
|
|
1652
|
+
.requiredOption("--category <category>", "inspected catalog category")
|
|
1652
1653
|
.requiredOption("--platforms <ids>", "explicitly reviewed comma-separated platforms: windows,macos,linux")
|
|
1653
1654
|
.option("--display-name <name>", "reviewed display name")
|
|
1654
1655
|
.option("--description <text>", "reviewed description")
|
|
@@ -2590,13 +2591,12 @@ program
|
|
|
2590
2591
|
});
|
|
2591
2592
|
console.log(formatPreparedCatalogInstall(prepared));
|
|
2592
2593
|
if (!options.yes) {
|
|
2593
|
-
console.log(
|
|
2594
|
+
console.log(formatCatalogApplyGuidance(prepared.entries.some((entry) => entry.safety.approvalRequired)));
|
|
2594
2595
|
return;
|
|
2595
2596
|
}
|
|
2596
2597
|
const snapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
2597
2598
|
approveRisk: options.approveRisk,
|
|
2598
2599
|
});
|
|
2599
|
-
await recordInstalledProfile(prepared);
|
|
2600
2600
|
console.log(`Installed ${prepared.entries.length} repositories as one transaction. Snapshot: ${snapshotId}`);
|
|
2601
2601
|
return;
|
|
2602
2602
|
}
|
|
@@ -2671,7 +2671,9 @@ program
|
|
|
2671
2671
|
const chosen = options.snapshot ?? snapshotIds.at(-1);
|
|
2672
2672
|
if (!chosen)
|
|
2673
2673
|
throw new Error("No Loadout snapshots found");
|
|
2674
|
-
await restoreSnapshot(await readSnapshot(chosen)
|
|
2674
|
+
await restoreSnapshot(await readSnapshot(chosen), {
|
|
2675
|
+
requireUnchangedPostMutationState: true,
|
|
2676
|
+
});
|
|
2675
2677
|
return chosen;
|
|
2676
2678
|
});
|
|
2677
2679
|
console.log(`Restored snapshot ${selected}`);
|
|
@@ -2716,7 +2718,6 @@ program
|
|
|
2716
2718
|
profileSnapshotId = await applyPreparedCatalogInstall(prepared, {
|
|
2717
2719
|
approveRisk: options.approveRisk,
|
|
2718
2720
|
});
|
|
2719
|
-
await recordInstalledProfile(prepared);
|
|
2720
2721
|
if (!options.json)
|
|
2721
2722
|
console.log(`Applied reviewed ${saved.mode} profile changes. Snapshot: ${profileSnapshotId}`);
|
|
2722
2723
|
profile = await evaluateInstalledProfile(catalog);
|
|
@@ -2883,7 +2884,12 @@ for (const command of program.commands)
|
|
|
2883
2884
|
if (HIDDEN_FROM_FIRST_SCREEN.has(command.name()))
|
|
2884
2885
|
command._hidden = true;
|
|
2885
2886
|
program.addHelpText("after", "\nStart here: `loadout guide` shows the safe everyday path. `loadout advanced` lists the retained maintainer and integration commands.\n");
|
|
2886
|
-
program.
|
|
2887
|
+
program.argument("[unknown-command]");
|
|
2888
|
+
program.action((unknownCommand) => {
|
|
2889
|
+
if (unknownCommand)
|
|
2890
|
+
throw new Error(`Unknown command '${unknownCommand}'. Run 'loadout --help' to list available commands.`);
|
|
2891
|
+
printBeginnerGuide();
|
|
2892
|
+
});
|
|
2887
2893
|
try {
|
|
2888
2894
|
await recoverPendingTransactions();
|
|
2889
2895
|
await program.parseAsync();
|
|
@@ -140,6 +140,16 @@ export function adapterCapabilities(agent) {
|
|
|
140
140
|
throw new Error(`No adapter capability declaration for '${agent}'`);
|
|
141
141
|
return found;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Return the display names of agents with an explicitly declared native skill
|
|
145
|
+
* directory. This is a filesystem-support boundary, not a claim about native
|
|
146
|
+
* application runtime behavior.
|
|
147
|
+
*/
|
|
148
|
+
export function supportedAdapterNames(adapters = ADAPTER_CAPABILITIES) {
|
|
149
|
+
return adapters
|
|
150
|
+
.filter((adapter) => adapter.components.skill === "native")
|
|
151
|
+
.map((adapter) => adapter.displayName);
|
|
152
|
+
}
|
|
143
153
|
/**
|
|
144
154
|
* Produce a read-only native-skill plan for one agent. This is deliberately
|
|
145
155
|
* narrower than package normalization: adapters may only target a directory
|
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
|
+
}
|