loadout-ai 0.1.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/MASTER_PLAN.md +1711 -0
  3. package/README.md +453 -0
  4. package/SECURITY.md +18 -0
  5. package/SIMPLE_PLAN.md +44 -0
  6. package/catalog/discovered.json +10469 -0
  7. package/catalog/packages.json +1150 -0
  8. package/dashboard/app.js +607 -0
  9. package/dashboard/index.html +249 -0
  10. package/dashboard/styles.css +384 -0
  11. package/dist/src/cli.js +2735 -0
  12. package/dist/src/core/active-policy.js +233 -0
  13. package/dist/src/core/active-set.js +382 -0
  14. package/dist/src/core/adapters.js +254 -0
  15. package/dist/src/core/adopt.js +92 -0
  16. package/dist/src/core/agent-health-score.js +475 -0
  17. package/dist/src/core/agent-inspection.js +152 -0
  18. package/dist/src/core/agent-versions.js +131 -0
  19. package/dist/src/core/api.js +90 -0
  20. package/dist/src/core/atomic-file.js +22 -0
  21. package/dist/src/core/audit.js +174 -0
  22. package/dist/src/core/benchmark-campaign.js +496 -0
  23. package/dist/src/core/benchmark-evidence.js +458 -0
  24. package/dist/src/core/benchmark-fixtures.js +1157 -0
  25. package/dist/src/core/benchmark-runner.js +301 -0
  26. package/dist/src/core/benchmark-trust.js +795 -0
  27. package/dist/src/core/canary.js +77 -0
  28. package/dist/src/core/candidate-intelligence.js +661 -0
  29. package/dist/src/core/catalog-coverage.js +85 -0
  30. package/dist/src/core/catalog-install.js +186 -0
  31. package/dist/src/core/catalog-release.js +286 -0
  32. package/dist/src/core/catalog.js +327 -0
  33. package/dist/src/core/codex-mcp.js +122 -0
  34. package/dist/src/core/community.js +86 -0
  35. package/dist/src/core/compatibility-intelligence.js +465 -0
  36. package/dist/src/core/completion.js +160 -0
  37. package/dist/src/core/components.js +138 -0
  38. package/dist/src/core/conversion.js +56 -0
  39. package/dist/src/core/credentials.js +377 -0
  40. package/dist/src/core/demo.js +136 -0
  41. package/dist/src/core/diff.js +83 -0
  42. package/dist/src/core/discovery-connector.js +165 -0
  43. package/dist/src/core/doctor.js +95 -0
  44. package/dist/src/core/ecosystem-import.js +1072 -0
  45. package/dist/src/core/evaluate.js +72 -0
  46. package/dist/src/core/file-lock.js +140 -0
  47. package/dist/src/core/freshness-alerts.js +230 -0
  48. package/dist/src/core/github-discovery.js +97 -0
  49. package/dist/src/core/github.js +148 -0
  50. package/dist/src/core/head-to-head.js +437 -0
  51. package/dist/src/core/health-score-evidence.js +169 -0
  52. package/dist/src/core/health.js +129 -0
  53. package/dist/src/core/improve.js +223 -0
  54. package/dist/src/core/install.js +233 -0
  55. package/dist/src/core/intelligence-feed-build.js +59 -0
  56. package/dist/src/core/intelligence-feed.js +353 -0
  57. package/dist/src/core/loadout-badge.js +55 -0
  58. package/dist/src/core/loadout-card.js +87 -0
  59. package/dist/src/core/manifest.js +307 -0
  60. package/dist/src/core/mcp-recipes.js +419 -0
  61. package/dist/src/core/mcp-registry-discovery.js +455 -0
  62. package/dist/src/core/mcp.js +287 -0
  63. package/dist/src/core/model-config.js +102 -0
  64. package/dist/src/core/observations.js +87 -0
  65. package/dist/src/core/outcomes.js +164 -0
  66. package/dist/src/core/package.js +225 -0
  67. package/dist/src/core/paths.js +214 -0
  68. package/dist/src/core/portable.js +130 -0
  69. package/dist/src/core/private-discovery.js +45 -0
  70. package/dist/src/core/profiles.js +255 -0
  71. package/dist/src/core/provenance.js +278 -0
  72. package/dist/src/core/ranking.js +104 -0
  73. package/dist/src/core/recommend.js +181 -0
  74. package/dist/src/core/registry-api.js +97 -0
  75. package/dist/src/core/registry.js +316 -0
  76. package/dist/src/core/release-claims.js +175 -0
  77. package/dist/src/core/remove.js +122 -0
  78. package/dist/src/core/review-queue.js +200 -0
  79. package/dist/src/core/runtime-tool-recipe.js +315 -0
  80. package/dist/src/core/runtime-tools.js +453 -0
  81. package/dist/src/core/safety.js +234 -0
  82. package/dist/src/core/sandbox.js +78 -0
  83. package/dist/src/core/scheduler.js +264 -0
  84. package/dist/src/core/share-report.js +85 -0
  85. package/dist/src/core/signing.js +115 -0
  86. package/dist/src/core/skill-compare.js +239 -0
  87. package/dist/src/core/skill-inventory.js +193 -0
  88. package/dist/src/core/skill-security.js +340 -0
  89. package/dist/src/core/skills-sh-discovery.js +336 -0
  90. package/dist/src/core/skills.js +208 -0
  91. package/dist/src/core/snapshot.js +210 -0
  92. package/dist/src/core/source.js +305 -0
  93. package/dist/src/core/state.js +300 -0
  94. package/dist/src/core/sync.js +348 -0
  95. package/dist/src/core/terminal.js +53 -0
  96. package/dist/src/core/transaction.js +211 -0
  97. package/dist/src/core/update-watch.js +39 -0
  98. package/dist/src/core/update.js +216 -0
  99. package/dist/src/core/upgrade.js +207 -0
  100. package/dist/src/dashboard.js +418 -0
  101. package/dist/src/shared/schemas.js +336 -0
  102. package/dist/src/shared/types.js +1 -0
  103. package/docs/ACTIVE_SET.md +53 -0
  104. package/docs/ACTIVE_SET_POLICY.md +45 -0
  105. package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
  106. package/docs/CATALOG.md +78 -0
  107. package/docs/CATALOG_POLICY.md +41 -0
  108. package/docs/COMMUNITY_DISCOVERY.md +14 -0
  109. package/docs/COMPATIBILITY_POLICY.md +22 -0
  110. package/docs/CONVERSION_AND_SANDBOX.md +27 -0
  111. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
  112. package/docs/DISCOVERED.md +280 -0
  113. package/docs/EVALUATION_PROTOCOL.md +40 -0
  114. package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
  115. package/docs/FEATURE_TEST_MATRIX.md +802 -0
  116. package/docs/GITHUB_AUTHORIZATION.md +38 -0
  117. package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
  118. package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
  119. package/docs/PROVIDER_CONFIGURATION.md +45 -0
  120. package/docs/RELEASE_REVIEW.md +117 -0
  121. package/docs/SAFE_UPDATE_DEMO.md +25 -0
  122. package/docs/SCHEMA_DECISIONS.md +25 -0
  123. package/docs/TEAM_POLICY.md +18 -0
  124. package/docs/TESTING.md +190 -0
  125. package/package.json +82 -0
@@ -0,0 +1,216 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { fetchRepositorySnapshot } from "./source.js";
4
+ import { repositoryCachePath } from "./source.js";
5
+ import { diffRepositorySnapshots } from "./diff.js";
6
+ import { hashDirectory, readInstallState } from "./state.js";
7
+ import { analyzeUpdateSafety } from "./safety.js";
8
+ import { detectAgents, loadoutHome } from "./paths.js";
9
+ import { applySkillInstall, buildSkillPlan, installedAgents, } from "./install.js";
10
+ import { validateSkillDirectory } from "./skills.js";
11
+ /** Builds a read-only update plan from persisted installs and live GitHub snapshots. */
12
+ export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository)) {
13
+ const state = await readInstallState();
14
+ return Promise.all(state.installs.map(async (record) => {
15
+ const disabledAgents = (state.activations ?? [])
16
+ .filter((activation) => activation.packageId === record.packageId &&
17
+ activation.installationState === "installed" &&
18
+ activation.activationState === "disabled")
19
+ .map((activation) => activation.agent);
20
+ const base = {
21
+ packageId: record.packageId,
22
+ repository: record.repository,
23
+ installedCommit: record.resolvedCommit,
24
+ targetAgents: record.targetAgents,
25
+ ...(disabledAgents.length ? { disabledAgents } : {}),
26
+ };
27
+ if (!record.repository || !record.resolvedCommit) {
28
+ return {
29
+ ...base,
30
+ status: "untracked",
31
+ action: "Reinstall from the original source to begin update tracking.",
32
+ };
33
+ }
34
+ try {
35
+ const current = await resolver(record.repository);
36
+ const same = current.commit.toLowerCase() === record.resolvedCommit.toLowerCase();
37
+ let diff;
38
+ let safetyFindings;
39
+ let approvalRequired = false;
40
+ if (!same && current.path) {
41
+ const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
42
+ diff = await diffRepositorySnapshots(oldPath, current.path);
43
+ const safety = await analyzeUpdateSafety(oldPath, current.path);
44
+ safetyFindings = safety.findings;
45
+ approvalRequired = safety.approvalRequired;
46
+ }
47
+ return {
48
+ ...base,
49
+ availableCommit: current.commit,
50
+ status: same ? "up-to-date" : "update-available",
51
+ action: same
52
+ ? "No action required."
53
+ : disabledAgents.length
54
+ ? `Enable ${record.packageId} for ${disabledAgents.join(", ")} before applying an update; planning remains read-only.`
55
+ : approvalRequired
56
+ ? `Approval required: review safety warnings before updating ${record.packageId}.`
57
+ : `Run loadout update --package ${record.packageId} after review.`,
58
+ ...(approvalRequired ? { approvalRequired: true } : {}),
59
+ ...(safetyFindings?.length ? { safetyFindings } : {}),
60
+ ...(diff ? { diff } : {}),
61
+ };
62
+ }
63
+ catch (error) {
64
+ return {
65
+ ...base,
66
+ status: "error",
67
+ action: "Retry when GitHub is reachable; the installed version was not changed.",
68
+ error: error instanceof Error ? error.message : String(error),
69
+ };
70
+ }
71
+ }));
72
+ }
73
+ export function formatUpdatePlan(plans) {
74
+ if (plans.length === 0)
75
+ return "No tracked installations found.";
76
+ return plans
77
+ .map((plan) => {
78
+ const suffix = plan.error ? ` — ${plan.error}` : "";
79
+ const repo = plan.repository ? ` (${plan.repository})` : "";
80
+ const safety = plan.safetyFindings
81
+ ?.map((finding) => ` [${finding.severity}] ${finding.message}${finding.names?.length ? ` (${finding.names.join(", ")})` : ""}`)
82
+ .join("") ?? "";
83
+ return `${plan.status.toUpperCase()} ${plan.packageId}${repo}: ${plan.action}${safety}${suffix}`;
84
+ })
85
+ .join("\n");
86
+ }
87
+ function quarantineRoot() {
88
+ return join(loadoutHome(), "quarantine");
89
+ }
90
+ function safeName(value) {
91
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 160) || "update";
92
+ }
93
+ /**
94
+ * Record a blocked update in an isolated review directory. This writes metadata
95
+ * only; it never installs files, imports configuration, or executes repository code.
96
+ */
97
+ export async function quarantineUpdate(packageId, repository, commit, findings) {
98
+ const directory = join(quarantineRoot(), `${safeName(packageId)}-${safeName(commit)}`);
99
+ await mkdir(directory, { recursive: true });
100
+ const metadata = join(directory, "metadata.json");
101
+ await writeFile(metadata, JSON.stringify({
102
+ packageId,
103
+ repository,
104
+ commit,
105
+ quarantinedAt: new Date().toISOString(),
106
+ findings: findings.map(({ severity, category, message, paths, names }) => ({
107
+ severity,
108
+ category,
109
+ message,
110
+ paths,
111
+ ...(names?.length ? { names } : {}),
112
+ })),
113
+ }, null, 2) + "\n", { mode: 0o600 });
114
+ return directory;
115
+ }
116
+ /**
117
+ * Verify copied skill metadata only. This is deliberately static: Loadout does
118
+ * not run hooks, lifecycle scripts, MCP servers, or any repository code while
119
+ * applying an update.
120
+ */
121
+ async function verifyInstalledSkills(context) {
122
+ for (const file of context.plan.files)
123
+ await validateSkillDirectory(file.target);
124
+ }
125
+ /**
126
+ * Apply an already reviewed package update. A failed post-copy verification
127
+ * restores the transaction snapshot, including the previous install state.
128
+ */
129
+ export async function applyPackageUpdate(packageId, options = {}, runtime = {}) {
130
+ const state = await readInstallState();
131
+ const record = state.installs.find((item) => item.packageId === packageId);
132
+ if (!record)
133
+ throw new Error(`Package is not managed by Loadout: ${packageId}`);
134
+ const disabledAgents = (state.activations ?? [])
135
+ .filter((activation) => activation.packageId === packageId &&
136
+ activation.installationState === "installed" &&
137
+ activation.activationState === "disabled")
138
+ .map((activation) => activation.agent);
139
+ const expectedRecord = JSON.stringify(record);
140
+ const expectedActivations = JSON.stringify((state.activations ?? [])
141
+ .filter((activation) => activation.packageId === packageId)
142
+ .sort((left, right) => `${left.agent}\0${left.unitId ?? ""}`.localeCompare(`${right.agent}\0${right.unitId ?? ""}`)));
143
+ if (disabledAgents.length)
144
+ throw new Error(`Package '${packageId}' is disabled for ${disabledAgents.join(", ")}. Enable it before updating so an update cannot silently reactivate agent-visible files.`);
145
+ if (!record.repository || !record.resolvedCommit)
146
+ throw new Error(`Package '${packageId}' has no tracked GitHub source`);
147
+ const current = await (runtime.fetchSnapshot ?? fetchRepositorySnapshot)(record.repository);
148
+ if (current.commit.toLowerCase() === record.resolvedCommit.toLowerCase())
149
+ throw new Error(`Package '${packageId}' is already up to date`);
150
+ const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
151
+ const safety = await analyzeUpdateSafety(oldPath, current.path);
152
+ if (safety.approvalRequired && !options.approveRisk) {
153
+ const quarantinePath = options.quarantineOnBlock === false
154
+ ? undefined
155
+ : await quarantineUpdate(packageId, current.repository, current.commit, safety.findings);
156
+ throw new Error(`Update is blocked pending explicit risk approval${quarantinePath ? `; quarantined at ${quarantinePath}` : ""}: ${safety.findings
157
+ .filter((finding) => finding.severity === "blocking")
158
+ .map((finding) => finding.message)
159
+ .join(" ")}`);
160
+ }
161
+ const agents = installedAgents(await (runtime.detectAgents ?? detectAgents)(), record.targetAgents);
162
+ const plan = await (runtime.buildPlan ?? buildSkillPlan)(current.path, record.packageId, agents);
163
+ const verifier = runtime.verify ?? verifyInstalledSkills;
164
+ let verificationFailure;
165
+ let verificationSnapshotId;
166
+ try {
167
+ const snapshotId = await applySkillInstall(plan, {
168
+ repository: current.repository,
169
+ resolvedCommit: current.commit,
170
+ }, {
171
+ allowManagedReplacement: true,
172
+ replaceManagedTargets: true,
173
+ validateCurrentState: async () => {
174
+ const freshState = await readInstallState();
175
+ const freshRecord = freshState.installs.find((entry) => entry.packageId === packageId);
176
+ const freshActivations = (freshState.activations ?? [])
177
+ .filter((activation) => activation.packageId === packageId)
178
+ .sort((left, right) => `${left.agent}\0${left.unitId ?? ""}`.localeCompare(`${right.agent}\0${right.unitId ?? ""}`));
179
+ if (JSON.stringify(freshRecord) !== expectedRecord ||
180
+ JSON.stringify(freshActivations) !== expectedActivations)
181
+ throw new Error(`Package '${packageId}' changed while its update was prepared; build the update plan again`);
182
+ const liveFiles = (await Promise.all([...new Set(plan.files.map((file) => file.target))].map(hashDirectory)))
183
+ .flat()
184
+ .sort((left, right) => left.path.localeCompare(right.path));
185
+ const expectedFiles = [...record.files].sort((left, right) => left.path.localeCompare(right.path));
186
+ if (JSON.stringify(liveFiles) !== JSON.stringify(expectedFiles))
187
+ throw new Error(`Package '${packageId}' files changed while its update was prepared; review local changes before updating`);
188
+ },
189
+ verifyBeforeCommit: async (pendingSnapshotId) => {
190
+ verificationSnapshotId = pendingSnapshotId;
191
+ try {
192
+ await verifier({
193
+ packageId,
194
+ commit: current.commit,
195
+ plan,
196
+ snapshotId: pendingSnapshotId,
197
+ });
198
+ }
199
+ catch (error) {
200
+ verificationFailure = error;
201
+ throw error;
202
+ }
203
+ },
204
+ });
205
+ return { snapshotId, commit: current.commit };
206
+ }
207
+ catch (error) {
208
+ if (verificationFailure !== undefined) {
209
+ const detail = verificationFailure instanceof Error
210
+ ? verificationFailure.message
211
+ : String(verificationFailure);
212
+ throw new Error(`Update verification failed; restored snapshot ${verificationSnapshotId}: ${detail}`, { cause: verificationFailure });
213
+ }
214
+ throw error;
215
+ }
216
+ }
@@ -0,0 +1,207 @@
1
+ import { buildAdapterCapabilityGaps, } from "./adapters.js";
2
+ import { applyPreparedCatalogInstall, formatPreparedCatalogInstall, prepareCatalogInstall, RECOMMENDED_ACTIVE_SKILL_LIMIT, } from "./catalog-install.js";
3
+ import { loadEffectiveCatalog } from "./catalog.js";
4
+ import { buildHealthReport, formatHealthReport } from "./health.js";
5
+ import { buildLocalAgentHealthScores } from "./health-score-evidence.js";
6
+ import { formatRecommendations, personalizeRecommendations, recommendPackages, scanProject, } from "./recommend.js";
7
+ import { readLocalOutcomes } from "./outcomes.js";
8
+ /**
9
+ * Builds the complete first-run preview without changing agent or Loadout state.
10
+ * Network access is limited to the same immutable catalog preparation performed by
11
+ * `setup`; the health and project scans remain local.
12
+ */
13
+ export async function planUpgrade(selection, options = {}) {
14
+ const projectPath = options.projectPath ?? process.cwd();
15
+ const catalog = options.catalog ?? (await loadEffectiveCatalog());
16
+ const health = options.health ?? (() => buildHealthReport());
17
+ const healthScores = options.healthScores ?? (() => buildLocalAgentHealthScores());
18
+ const projectScan = options.projectScan ?? scanProject;
19
+ const outcomes = options.outcomes ?? readLocalOutcomes;
20
+ const [healthBefore, healthScoresBefore, project, prepared, localOutcomes] = await Promise.all([
21
+ health(),
22
+ healthScores(),
23
+ projectScan(projectPath),
24
+ prepareCatalogInstall(selection, {
25
+ catalog,
26
+ ...(options.requestedAgents
27
+ ? { requestedAgents: options.requestedAgents }
28
+ : {}),
29
+ ...(options.detectedAgents
30
+ ? { detectedAgents: options.detectedAgents }
31
+ : {}),
32
+ ...(options.fetchSnapshot
33
+ ? { fetchSnapshot: options.fetchSnapshot }
34
+ : {}),
35
+ ...(options.onProgress ? { onProgress: options.onProgress } : {}),
36
+ }),
37
+ outcomes(),
38
+ ]);
39
+ const recommendations = recommendPackages(project, catalog);
40
+ const personalizedRecommendations = prepared.agents.map((agent) => ({
41
+ agent: agent.id,
42
+ recommendations: personalizeRecommendations(recommendations, project, localOutcomes, agent.id),
43
+ }));
44
+ const selectedAgents = new Set(prepared.agents.map((agent) => agent.id));
45
+ const capabilityGaps = buildAdapterCapabilityGaps().filter((gap) => selectedAgents.has(gap.agent));
46
+ const alternatives = [
47
+ ...prepared.resolution.conflicts.map((conflict) => ({
48
+ kind: "profile-conflict",
49
+ selected: conflict.defaultPackageId ?? conflict.packageIds[0] ?? "unresolved",
50
+ deferred: conflict.packageIds.filter((id) => id !== conflict.defaultPackageId),
51
+ reason: conflict.message,
52
+ })),
53
+ ...prepared.collisions.map((collision) => ({
54
+ kind: "target-overlap",
55
+ selected: collision.keptPackageId,
56
+ deferred: [collision.deferredPackageId],
57
+ reason: `Both packages target the same agent skill directory; ${collision.keptPackageId} wins by deterministic profile evidence order.`,
58
+ })),
59
+ ];
60
+ const deferredActions = prepared.skipped
61
+ .filter((item) => item.kind === "explicit-setup")
62
+ .map((item) => {
63
+ const pkg = prepared.resolution.packages.find((candidate) => candidate.id === item.packageId);
64
+ const components = pkg?.components ?? [];
65
+ return {
66
+ packageId: item.packageId,
67
+ components,
68
+ reason: item.reason,
69
+ nextCommand: components.includes("mcp")
70
+ ? `loadout mcp --repository ${pkg?.repository ?? item.packageId}`
71
+ : `loadout inspect ${item.packageId}`,
72
+ automatic: false,
73
+ };
74
+ });
75
+ const targetDirectories = prepared.entries.reduce((total, entry) => total + entry.plan.files.length, 0);
76
+ const plannedDirectoriesPerAgent = prepared.agents.length
77
+ ? Math.ceil(targetDirectories / prepared.agents.length)
78
+ : 0;
79
+ const activeSet = selection.mode === "maximum"
80
+ ? {
81
+ policy: "disabled-library",
82
+ recommendedLimit: RECOMMENDED_ACTIVE_SKILL_LIMIT,
83
+ plannedDirectoriesPerAgent,
84
+ exceedsRecommendedLimit: plannedDirectoriesPerAgent > RECOMMENDED_ACTIVE_SKILL_LIMIT,
85
+ explanation: "Maximum stores reviewed skills in the disabled library; it does not expose the full library to agent context.",
86
+ nextCommand: `loadout optimize --project ${project.root} --limit ${RECOMMENDED_ACTIVE_SKILL_LIMIT}`,
87
+ }
88
+ : {
89
+ policy: "bounded-active",
90
+ recommendedLimit: RECOMMENDED_ACTIVE_SKILL_LIMIT,
91
+ plannedDirectoriesPerAgent,
92
+ exceedsRecommendedLimit: plannedDirectoriesPerAgent > RECOMMENDED_ACTIVE_SKILL_LIMIT,
93
+ explanation: plannedDirectoriesPerAgent <= RECOMMENDED_ACTIVE_SKILL_LIMIT
94
+ ? "The prepared profile stays within the recommended active-set ceiling."
95
+ : "The prepared profile exceeds the recommended active-set ceiling; use Stable or project optimization before treating it as a daily active set.",
96
+ ...(plannedDirectoriesPerAgent > RECOMMENDED_ACTIVE_SKILL_LIMIT
97
+ ? {
98
+ nextCommand: `loadout optimize --project ${project.root} --limit ${RECOMMENDED_ACTIVE_SKILL_LIMIT}`,
99
+ }
100
+ : {}),
101
+ };
102
+ const riskApprovalRequired = prepared.entries.some((entry) => entry.safety.approvalRequired);
103
+ return {
104
+ schemaVersion: 1,
105
+ generatedAt: (options.now?.() ?? new Date()).toISOString(),
106
+ project,
107
+ recommendations,
108
+ personalizedRecommendations,
109
+ localOutcomeEventsConsidered: localOutcomes.events.length,
110
+ healthBefore,
111
+ healthScoresBefore,
112
+ prepared,
113
+ alternatives,
114
+ capabilityGaps,
115
+ deferredActions,
116
+ activeSet,
117
+ riskApprovalRequired,
118
+ guarantees: [
119
+ "preview is read-only until --yes",
120
+ "reviewed GitHub inputs resolve to exact catalog commits",
121
+ "all selected agent targets are changed through one snapshot-backed transaction",
122
+ "unmanaged agent content is not adopted, overwritten, or removed",
123
+ "daily discovery and update checks remain read-only and opt-in",
124
+ ],
125
+ };
126
+ }
127
+ export async function applyUpgrade(plan, options = {}) {
128
+ if (plan.riskApprovalRequired && !options.approveRisk)
129
+ throw new Error("The upgrade preview contains reviewed safety findings; inspect it and add --approve-risk");
130
+ const snapshotId = await applyPreparedCatalogInstall(plan.prepared, {
131
+ approveRisk: options.approveRisk,
132
+ });
133
+ const healthAfter = await (options.health ?? (() => buildHealthReport()))();
134
+ const healthScoresAfter = await (options.healthScores ?? (() => buildLocalAgentHealthScores()))();
135
+ return {
136
+ snapshotId,
137
+ healthBefore: plan.healthBefore,
138
+ healthAfter,
139
+ healthScoresBefore: plan.healthScoresBefore,
140
+ healthScoresAfter,
141
+ };
142
+ }
143
+ export function summarizeUpgradePlan(plan) {
144
+ return {
145
+ schemaVersion: plan.schemaVersion,
146
+ generatedAt: plan.generatedAt,
147
+ mode: plan.prepared.selection.mode,
148
+ project: plan.project,
149
+ healthBefore: plan.healthBefore,
150
+ healthScoresBefore: plan.healthScoresBefore,
151
+ recommendations: plan.recommendations,
152
+ personalizedRecommendations: plan.personalizedRecommendations,
153
+ localOutcomeEventsConsidered: plan.localOutcomeEventsConsidered,
154
+ agents: plan.prepared.agents,
155
+ repositories: plan.prepared.resolution.packages.map((pkg) => ({
156
+ id: pkg.id,
157
+ repository: pkg.repository,
158
+ commit: pkg.source?.commit,
159
+ license: pkg.license,
160
+ })),
161
+ install: {
162
+ repositoryCount: plan.prepared.entries.length,
163
+ targetDirectoryCount: plan.prepared.entries.reduce((total, entry) => total + entry.plan.files.length, 0),
164
+ skipped: plan.prepared.skipped,
165
+ collisions: plan.prepared.collisions,
166
+ riskApprovalRequired: plan.riskApprovalRequired,
167
+ safetyFindings: plan.prepared.entries.flatMap((entry) => entry.safety.findings.map((finding) => ({
168
+ packageId: entry.package.id,
169
+ ...finding,
170
+ }))),
171
+ },
172
+ alternatives: plan.alternatives,
173
+ capabilityGaps: plan.capabilityGaps,
174
+ deferredActions: plan.deferredActions,
175
+ activeSet: plan.activeSet,
176
+ guarantees: plan.guarantees,
177
+ };
178
+ }
179
+ export function formatUpgradePlan(plan) {
180
+ return [
181
+ "Loadout upgrade preview",
182
+ "",
183
+ formatHealthReport(plan.healthBefore),
184
+ ...plan.healthScoresBefore.map((score) => `Agent Health Score (${score.agent}): ${score.score}/100 (${score.rating}; evidence coverage ${score.evidenceCoverage}%)`),
185
+ "",
186
+ formatRecommendations(plan.project, plan.recommendations),
187
+ ...plan.personalizedRecommendations.flatMap((item) => item.recommendations.some((recommendation) => recommendation.localOutcomeAdjustment !== undefined)
188
+ ? [
189
+ "",
190
+ `Local outcome personalization (${item.agent}; ${plan.localOutcomeEventsConsidered} local event(s), never uploaded):`,
191
+ formatRecommendations(plan.project, item.recommendations),
192
+ ]
193
+ : []),
194
+ "",
195
+ formatPreparedCatalogInstall(plan.prepared),
196
+ "",
197
+ `Active-set policy: ${plan.activeSet.policy}; about ${plan.activeSet.plannedDirectoriesPerAgent}/${plan.activeSet.recommendedLimit} directories per agent. ${plan.activeSet.explanation}`,
198
+ ...(plan.activeSet.nextCommand
199
+ ? [`Next bounded activation: ${plan.activeSet.nextCommand}`]
200
+ : []),
201
+ ...plan.alternatives.map((item) => `Alternative (${item.kind}): selected ${item.selected}; deferred ${item.deferred.join(", ")} — ${item.reason}`),
202
+ ...plan.deferredActions.map((item) => `Deferred explicit action: ${item.packageId} [${item.components.join(", ") || "unknown"}] — ${item.nextCommand}`),
203
+ `Unsupported selected-agent capability combinations: ${plan.capabilityGaps.length}; no undocumented path will be guessed.`,
204
+ "",
205
+ ...plan.guarantees.map((guarantee) => `Safety: ${guarantee}`),
206
+ ].join("\n");
207
+ }