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.
- package/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- 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
|
+
}
|