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.
Files changed (44) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/MASTER_PLAN.md +86 -13
  3. package/README.md +157 -284
  4. package/dashboard/app.js +4 -4
  5. package/dashboard/index.html +4 -4
  6. package/dist/src/cli.js +27 -21
  7. package/dist/src/core/adapters.js +10 -0
  8. package/dist/src/core/adopt.js +165 -32
  9. package/dist/src/core/agent-health-score.js +2 -2
  10. package/dist/src/core/catalog-coverage.js +2 -1
  11. package/dist/src/core/catalog-install.js +8 -1
  12. package/dist/src/core/catalog-release.js +2 -1
  13. package/dist/src/core/conformance.js +74 -0
  14. package/dist/src/core/install.js +36 -3
  15. package/dist/src/core/profiles.js +9 -4
  16. package/dist/src/core/ranking.js +1 -1
  17. package/dist/src/core/readme-claims.js +10 -0
  18. package/dist/src/core/readme-facts.js +40 -0
  19. package/dist/src/core/recommend.js +9 -3
  20. package/dist/src/core/runtime-tools.js +5 -2
  21. package/dist/src/core/scheduler.js +2 -1
  22. package/dist/src/core/snapshot.js +58 -13
  23. package/dist/src/core/state.js +8 -1
  24. package/dist/src/core/transaction.js +2 -1
  25. package/dist/src/core/uninstall.js +26 -2
  26. package/dist/src/dashboard.js +5 -2
  27. package/dist/src/shared/schemas.js +57 -0
  28. package/docs/FEATURE_TEST_MATRIX.md +16 -0
  29. package/docs/README_RESEARCH.md +36 -0
  30. package/docs/RELEASE_REVIEW.md +31 -5
  31. package/docs/REPOSITORY_STABILIZATION.md +190 -0
  32. package/docs/TESTING.md +50 -0
  33. package/docs/USER_TEST_GUIDE.md +26 -0
  34. package/docs/assets/loadout-hero.svg +259 -0
  35. package/docs/assets/loadout-mark.svg +54 -0
  36. package/docs/evidence/live-checks-2026-07-19.json +22 -0
  37. package/docs/evidence/live-checks.schema.json +28 -0
  38. package/docs/evidence/readme-claims.json +286 -0
  39. package/docs/superpowers/plans/2026-07-19-relatable-readme-hero.md +283 -0
  40. package/docs/superpowers/specs/2026-07-19-relatable-readme-hero-design.md +80 -0
  41. package/package.json +8 -4
  42. package/SIMPLE_PLAN.md +0 -44
  43. package/docs/plans/2026-07-18-release-0.3.md +0 -42
  44. 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, recordInstalledProfile, } from "./core/profile-state.js";
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 the recommended daily driver, `--mode power` for broader opt-in skills, or `--mode maximum` to download the screened library without activating it.");
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 (recommended), [2] Power Boost, [3] Maximum Library, [4] Custom: ")).trim();
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("Preview complete; nothing was changed. Re-run with --yes to install this exact screened plan.");
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.3.1";
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 reviewed library skills for a project"
734
- : "Scan a project and propose the best reviewed active-set additions")
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 tested Loadout profiles or inspect one profile")
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 reviewed catalog skill index")
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 reviewed catalog skill index")
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>", "reviewed catalog 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("Preview complete; nothing was changed. Use --yes after reviewing to install this exact plan.");
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.action(printBeginnerGuide);
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
@@ -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
- return {
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
- provenance: match.provenance,
39
- reviewed: Boolean(exact),
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
- const current = createHash("sha256")
67
- .update(await readFile(join(plan.path, "SKILL.md")))
68
- .digest("hex");
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: plan };
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: ${plan.reviewed ? "reviewed exact catalog match" : "unreviewed"}`,
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 reviewed library and re-check capacity after activation changes.",
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
- `Agent Health Score: ${score.score}/${score.maximumScore} (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`,
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
- `Recommendation trust: ${report.recommendedRecords} Stable sources · human license approval still required before release`,
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
+ }