loadout-ai 0.5.4 → 0.5.5
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 +22 -0
- package/MASTER_PLAN.md +10 -0
- package/README.md +2 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/core/health.js +16 -4
- package/dist/src/core/profile-state.js +21 -10
- package/dist/src/core/provenance.js +3 -1
- package/dist/src/core/update.js +52 -18
- package/docs/USER_TEST_GUIDE.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.5 - 2026-07-21
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Summarize update checks as active, disabled-library, current, and unavailable
|
|
10
|
+
records; hide repetitive current-package lines by default while retaining complete
|
|
11
|
+
JSON output and focused per-package review.
|
|
12
|
+
- Keep disabled Maximum-library copies pinned and explicitly state that newer
|
|
13
|
+
upstream commits neither change nor reactivate agent-visible skills.
|
|
14
|
+
- Treat catalog sources represented by adopted units as present in saved-profile
|
|
15
|
+
evaluation, and describe fully quarantined or unavailable profile sources without
|
|
16
|
+
calling them missing upgrades.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Deduplicate per-agent disabled update state and fetch a shared repository only once
|
|
21
|
+
when several adopted skill units track the same upstream source.
|
|
22
|
+
- Distinguish active updates from disabled-library updates in health output, including
|
|
23
|
+
separately reported network-check failures.
|
|
24
|
+
- Label partial catalog indexing accurately as sources containing quarantined units
|
|
25
|
+
instead of repository failures.
|
|
26
|
+
|
|
5
27
|
## 0.5.4 - 2026-07-21
|
|
6
28
|
|
|
7
29
|
### Added
|
package/MASTER_PLAN.md
CHANGED
|
@@ -318,6 +318,16 @@ merged `codex/relatable-readme-hero` remote branch remains safe to delete after
|
|
|
318
318
|
Humanizer and Turnstile update findings before any replacement, then finish
|
|
319
319
|
read-only update/discovery, Graphify removal, complete uninstall, and clean
|
|
320
320
|
reinstall acceptance.
|
|
321
|
+
- [x] `P18-45 [TERRA]` Correct the founder-discovered update-reporting defects after
|
|
322
|
+
the real `0.5.4` Maximum/reconciliation run. Update planning now deduplicates
|
|
323
|
+
repeated agent names and shared-repository fetches, separates active updates
|
|
324
|
+
from newer commits held for disabled library copies, compacts safety output,
|
|
325
|
+
recognizes adopted sources in profile state, and labels partial quarantine
|
|
326
|
+
evidence instead of calling it a repository failure. The real profile confirms
|
|
327
|
+
39 active managed skills, 2,326 disabled copies, one Graphify runtime, zero
|
|
328
|
+
managed drift, and no active adopted-skill update. Release this correction
|
|
329
|
+
before the remaining Graphify removal, discovery, uninstall, and reinstall
|
|
330
|
+
acceptance steps.
|
|
321
331
|
|
|
322
332
|
### Release 0.3 lifecycle hardening
|
|
323
333
|
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
> [!IMPORTANT]
|
|
29
|
-
> Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.
|
|
29
|
+
> Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.5`; review the preview before every apply.
|
|
30
30
|
|
|
31
31
|
## How it works
|
|
32
32
|
|
|
@@ -76,7 +76,7 @@ Skills, plugins, MCP servers, and agent settings tend to accumulate one experime
|
|
|
76
76
|
You need Node.js 20 or newer and Git.
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
npm install --global loadout-ai@0.5.
|
|
79
|
+
npm install --global loadout-ai@0.5.5
|
|
80
80
|
loadout --version
|
|
81
81
|
loadout guide
|
|
82
82
|
```
|
package/dist/src/cli.js
CHANGED
|
@@ -264,7 +264,7 @@ async function runSetup(options) {
|
|
|
264
264
|
reader?.close();
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
-
const LOADOUT_VERSION = "0.5.
|
|
267
|
+
const LOADOUT_VERSION = "0.5.5";
|
|
268
268
|
function durableSchedulerLauncher() {
|
|
269
269
|
return [
|
|
270
270
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
package/dist/src/core/health.js
CHANGED
|
@@ -103,15 +103,24 @@ export async function buildHealthReport(options = {}) {
|
|
|
103
103
|
fix: "Review the target, then remove and reinstall the owning runtime tool.",
|
|
104
104
|
});
|
|
105
105
|
const available = updates.filter((update) => update.status === "update-available");
|
|
106
|
-
|
|
106
|
+
const disabledAvailable = available.filter((update) => Boolean(update.disabledAgents?.length));
|
|
107
|
+
const activeAvailable = available.filter((update) => !update.disabledAgents?.length);
|
|
108
|
+
if (activeAvailable.length)
|
|
107
109
|
findings.push({
|
|
108
|
-
level:
|
|
110
|
+
level: activeAvailable.some((update) => update.approvalRequired)
|
|
109
111
|
? "warning"
|
|
110
112
|
: "info",
|
|
111
113
|
code: "updates-available",
|
|
112
|
-
message: `${
|
|
114
|
+
message: `${activeAvailable.length} active package update(s) are available.`,
|
|
113
115
|
fix: "Run loadout update and review the safety findings.",
|
|
114
116
|
});
|
|
117
|
+
if (disabledAvailable.length)
|
|
118
|
+
findings.push({
|
|
119
|
+
level: "info",
|
|
120
|
+
code: "disabled-library-updates-available",
|
|
121
|
+
message: `${disabledAvailable.length} disabled-library package(s) have newer upstream commits.`,
|
|
122
|
+
fix: "Nothing active changed; disabled skills stay pinned and are never reactivated automatically.",
|
|
123
|
+
});
|
|
115
124
|
const errors = updates.filter((update) => update.status === "error");
|
|
116
125
|
if (errors.length)
|
|
117
126
|
findings.push({
|
|
@@ -150,6 +159,9 @@ export async function buildHealthReport(options = {}) {
|
|
|
150
159
|
managedRuntimeTools: runtimeTools.length,
|
|
151
160
|
updatesChecked: Boolean(options.updates || options.checkUpdates),
|
|
152
161
|
updatesAvailable: available.length,
|
|
162
|
+
activeUpdatesAvailable: activeAvailable.length,
|
|
163
|
+
disabledUpdatesAvailable: disabledAvailable.length,
|
|
164
|
+
updateChecksFailed: errors.length,
|
|
153
165
|
driftedFiles: drifted.length,
|
|
154
166
|
driftedMcpServers,
|
|
155
167
|
findings,
|
|
@@ -165,7 +177,7 @@ export function formatHealthReport(report) {
|
|
|
165
177
|
: "✗";
|
|
166
178
|
const lines = [
|
|
167
179
|
`${icon} Loadout health: ${report.status === "not-configured" ? "not configured" : report.status === "library-only" ? "library ready (nothing active)" : report.status}`,
|
|
168
|
-
`Packages: ${report.installedPackages} managed; skills: ${report.activeSkills ?? 0} active, ${report.disabledSkills ?? 0} disabled; MCP servers: ${report.managedMcpServers ?? 0}; runtime tools: ${report.managedRuntimeTools ?? 0}; ${report.updatesChecked ? `${report.updatesAvailable} update(s)` : "updates not checked (use --updates)"}; ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
|
|
180
|
+
`Packages: ${report.installedPackages} managed; skills: ${report.activeSkills ?? 0} active, ${report.disabledSkills ?? 0} disabled; MCP servers: ${report.managedMcpServers ?? 0}; runtime tools: ${report.managedRuntimeTools ?? 0}; ${report.updatesChecked ? `${report.activeUpdatesAvailable ?? report.updatesAvailable} active update(s), ${report.disabledUpdatesAvailable ?? 0} disabled-library update(s), ${report.updateChecksFailed ?? 0} check(s) unavailable` : "updates not checked (use --updates)"}; ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
|
|
169
181
|
];
|
|
170
182
|
for (const finding of report.findings)
|
|
171
183
|
lines.push(`${finding.level === "ok" ? "✓" : finding.level === "error" ? "✗" : finding.level === "warning" ? "!" : "•"} ${finding.message}${finding.fix ? ` ${finding.fix}` : ""}`);
|
|
@@ -27,6 +27,7 @@ export function evaluateInstalledProfileState(state, catalog) {
|
|
|
27
27
|
installed: false,
|
|
28
28
|
expectedPackages: [],
|
|
29
29
|
missingPackages: [],
|
|
30
|
+
newlyReviewedPackages: [],
|
|
30
31
|
reviewedRevisionChanges: [],
|
|
31
32
|
needsRefresh: false,
|
|
32
33
|
evaluatedAt,
|
|
@@ -41,8 +42,16 @@ export function evaluateInstalledProfileState(state, catalog) {
|
|
|
41
42
|
const expected = resolution.packages.filter((pkg) => pkg.components?.includes("skill"));
|
|
42
43
|
const installed = new Map(state.installs.map((record) => [record.packageId, record]));
|
|
43
44
|
const previousCatalog = new Map(state.profile.catalogPackages.map((record) => [record.packageId, record]));
|
|
44
|
-
const
|
|
45
|
-
.
|
|
45
|
+
const representedRepositories = new Set(state.installs
|
|
46
|
+
.map((record) => record.repository?.toLowerCase())
|
|
47
|
+
.filter((repository) => Boolean(repository)));
|
|
48
|
+
const missingPackages = state.profile.catalogPackages
|
|
49
|
+
.filter((item) => !installed.has(item.packageId))
|
|
50
|
+
.map((item) => item.packageId);
|
|
51
|
+
const newlyReviewedPackages = expected
|
|
52
|
+
.filter((pkg) => !previousCatalog.has(pkg.id) &&
|
|
53
|
+
!installed.has(pkg.id) &&
|
|
54
|
+
!representedRepositories.has(pkg.repository.toLowerCase()))
|
|
46
55
|
.map((pkg) => pkg.id);
|
|
47
56
|
const reviewedRevisionChanges = expected.flatMap((pkg) => {
|
|
48
57
|
const previous = previousCatalog.get(pkg.id);
|
|
@@ -58,21 +67,16 @@ export function evaluateInstalledProfileState(state, catalog) {
|
|
|
58
67
|
},
|
|
59
68
|
];
|
|
60
69
|
});
|
|
61
|
-
const oldIds = state.profile.catalogPackages
|
|
62
|
-
.map((item) => item.packageId)
|
|
63
|
-
.sort();
|
|
64
70
|
const expectedPackages = expected.map((pkg) => pkg.id).sort();
|
|
65
|
-
const selectionChanged = oldIds.join("\0") !== expectedPackages.join("\0");
|
|
66
71
|
return {
|
|
67
72
|
installed: true,
|
|
68
73
|
mode: state.profile.mode,
|
|
69
74
|
appliedAt: state.profile.appliedAt,
|
|
70
75
|
expectedPackages,
|
|
71
76
|
missingPackages,
|
|
77
|
+
newlyReviewedPackages,
|
|
72
78
|
reviewedRevisionChanges,
|
|
73
|
-
needsRefresh:
|
|
74
|
-
missingPackages.length > 0 ||
|
|
75
|
-
reviewedRevisionChanges.length > 0,
|
|
79
|
+
needsRefresh: newlyReviewedPackages.length > 0 || reviewedRevisionChanges.length > 0,
|
|
76
80
|
evaluatedAt,
|
|
77
81
|
boundary,
|
|
78
82
|
};
|
|
@@ -87,7 +91,14 @@ export function formatInstalledProfileStatus(status) {
|
|
|
87
91
|
`PROFILE ${status.mode?.toUpperCase()}: ${status.needsRefresh ? "reviewed changes available" : "current against reviewed catalog"}`,
|
|
88
92
|
`Expected repositories: ${status.expectedPackages.length}`,
|
|
89
93
|
...(status.missingPackages.length
|
|
90
|
-
? [
|
|
94
|
+
? [
|
|
95
|
+
`Not installed from the saved profile (removed, quarantined, or unavailable): ${status.missingPackages.join(", ")}`,
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
98
|
+
...(status.newlyReviewedPackages.length
|
|
99
|
+
? [
|
|
100
|
+
`New reviewed catalog sources to inspect: ${status.newlyReviewedPackages.join(", ")}`,
|
|
101
|
+
]
|
|
91
102
|
: []),
|
|
92
103
|
...(status.reviewedRevisionChanges.length
|
|
93
104
|
? [
|
|
@@ -292,8 +292,10 @@ export function enrichInventoryWithProvenance(report, index, source = "none") {
|
|
|
292
292
|
}
|
|
293
293
|
export function formatProvenanceSummary(report) {
|
|
294
294
|
const value = report.provenance;
|
|
295
|
+
const quarantinedSources = value.failures.filter((failure) => /^\d+ skill unit\(s\) quarantined:/.test(failure.error)).length;
|
|
296
|
+
const preparationFailures = value.failures.length - quarantinedSources;
|
|
295
297
|
return [
|
|
296
298
|
`Provenance: ${value.exact} exact catalog match(es), ${value.managed} Loadout-managed, ${value.embedded} embedded-source candidate(s), ${value.nameCandidates} name-only candidate(s), ${value.unknown} unknown`,
|
|
297
|
-
`Catalog skill index: ${value.indexSource}; ${value.indexedSkills} reviewed skill(s)${
|
|
299
|
+
`Catalog skill index: ${value.indexSource}; ${value.indexedSkills} reviewed skill(s)${quarantinedSources ? `; ${quarantinedSources} source(s) contain quarantined units` : ""}${preparationFailures ? `; ${preparationFailures} source preparation failure(s)` : ""}`,
|
|
298
300
|
].join("\n");
|
|
299
301
|
}
|
package/dist/src/core/update.js
CHANGED
|
@@ -91,6 +91,15 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
91
91
|
? state.installs.filter((record) => record.packageId === options.packageId)
|
|
92
92
|
: state.installs;
|
|
93
93
|
const results = new Array(records.length);
|
|
94
|
+
const resolutions = new Map();
|
|
95
|
+
const resolveOnce = (repository) => {
|
|
96
|
+
const existing = resolutions.get(repository);
|
|
97
|
+
if (existing)
|
|
98
|
+
return existing;
|
|
99
|
+
const pending = resolver(repository);
|
|
100
|
+
resolutions.set(repository, pending);
|
|
101
|
+
return pending;
|
|
102
|
+
};
|
|
94
103
|
let cursor = 0;
|
|
95
104
|
let completed = 0;
|
|
96
105
|
const workers = Array.from({
|
|
@@ -100,17 +109,21 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
100
109
|
const index = cursor++;
|
|
101
110
|
const record = records[index];
|
|
102
111
|
results[index] = await (async () => {
|
|
103
|
-
const
|
|
104
|
-
.filter((activation) => activation.packageId === record.packageId &&
|
|
112
|
+
const disabledActivations = (state.activations ?? []).filter((activation) => activation.packageId === record.packageId &&
|
|
105
113
|
activation.installationState === "installed" &&
|
|
106
|
-
activation.activationState === "disabled")
|
|
107
|
-
|
|
114
|
+
activation.activationState === "disabled");
|
|
115
|
+
const disabledAgents = [
|
|
116
|
+
...new Set(disabledActivations.map((activation) => activation.agent)),
|
|
117
|
+
].sort();
|
|
108
118
|
const base = {
|
|
109
119
|
packageId: record.packageId,
|
|
110
120
|
repository: record.repository,
|
|
111
121
|
installedCommit: record.resolvedCommit,
|
|
112
122
|
targetAgents: record.targetAgents,
|
|
113
123
|
...(disabledAgents.length ? { disabledAgents } : {}),
|
|
124
|
+
...(disabledActivations.length
|
|
125
|
+
? { disabledUnits: disabledActivations.length }
|
|
126
|
+
: {}),
|
|
114
127
|
};
|
|
115
128
|
if (!record.repository || !record.resolvedCommit) {
|
|
116
129
|
return {
|
|
@@ -120,7 +133,7 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
120
133
|
};
|
|
121
134
|
}
|
|
122
135
|
try {
|
|
123
|
-
const current = await
|
|
136
|
+
const current = await resolveOnce(record.repository);
|
|
124
137
|
const same = current.commit.toLowerCase() ===
|
|
125
138
|
record.resolvedCommit.toLowerCase();
|
|
126
139
|
let diff;
|
|
@@ -140,7 +153,7 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
140
153
|
action: same
|
|
141
154
|
? "No action required."
|
|
142
155
|
: disabledAgents.length
|
|
143
|
-
? `
|
|
156
|
+
? `A newer upstream commit exists, but ${disabledActivations.length} reviewed library ${disabledActivations.length === 1 ? "copy is" : "copies are"} disabled for ${disabledAgents.join(" and ")}. Nothing active changed; Loadout will not update or reactivate disabled skills automatically.`
|
|
144
157
|
: approvalRequired
|
|
145
158
|
? `Approval required: review safety warnings before updating ${record.packageId}.`
|
|
146
159
|
: `Run loadout update --package ${record.packageId} --yes after review.`,
|
|
@@ -172,16 +185,35 @@ export async function buildUpdatePlan(resolver = async (repository) => fetchRepo
|
|
|
172
185
|
export function formatUpdatePlan(plans) {
|
|
173
186
|
if (plans.length === 0)
|
|
174
187
|
return "No tracked installations found.";
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
const available = plans.filter((plan) => plan.status === "update-available");
|
|
189
|
+
const activeAvailable = available.filter((plan) => !plan.disabledAgents?.length);
|
|
190
|
+
const disabledAvailable = available.filter((plan) => Boolean(plan.disabledAgents?.length));
|
|
191
|
+
const current = plans.filter((plan) => plan.status === "up-to-date");
|
|
192
|
+
const unavailable = plans.filter((plan) => plan.status === "error" || plan.status === "untracked");
|
|
193
|
+
const lines = [
|
|
194
|
+
`Update check: ${activeAvailable.length} active update(s), ${disabledAvailable.length} disabled-library update(s), ${current.length} current, ${unavailable.length} unavailable.`,
|
|
195
|
+
];
|
|
196
|
+
const visible = plans.length === 1
|
|
197
|
+
? plans
|
|
198
|
+
: plans.filter((plan) => plan.status !== "up-to-date");
|
|
199
|
+
lines.push(...visible.map((plan) => {
|
|
177
200
|
const suffix = plan.error ? ` — ${plan.error}` : "";
|
|
178
201
|
const repo = plan.repository ? ` (${plan.repository})` : "";
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
.
|
|
202
|
+
const findingCounts = new Map();
|
|
203
|
+
for (const finding of plan.safetyFindings ?? []) {
|
|
204
|
+
const key = `${finding.severity} ${finding.category}`;
|
|
205
|
+
findingCounts.set(key, (findingCounts.get(key) ?? 0) + 1);
|
|
206
|
+
}
|
|
207
|
+
const safety = findingCounts.size
|
|
208
|
+
? ` Safety review: ${[...findingCounts.entries()]
|
|
209
|
+
.map(([key, count]) => `${key}${count > 1 ? ` ×${count}` : ""}`)
|
|
210
|
+
.join(", ")}. Run loadout update --package ${plan.packageId} for the focused review.`
|
|
211
|
+
: "";
|
|
182
212
|
return `${plan.status.toUpperCase()} ${plan.packageId}${repo}: ${plan.action}${safety}${suffix}`;
|
|
183
|
-
})
|
|
184
|
-
|
|
213
|
+
}));
|
|
214
|
+
if (current.length && plans.length > 1)
|
|
215
|
+
lines.push(`Current packages hidden for readability (${current.length}); use --json for every record.`);
|
|
216
|
+
return lines.join("\n");
|
|
185
217
|
}
|
|
186
218
|
/** Updates safe enough for an explicit whole-profile `update --yes` apply. */
|
|
187
219
|
export function selectSafeAutomaticUpdates(plans) {
|
|
@@ -236,11 +268,13 @@ export async function applyPackageUpdate(packageId, options = {}, runtime = {})
|
|
|
236
268
|
const record = state.installs.find((item) => item.packageId === packageId);
|
|
237
269
|
if (!record)
|
|
238
270
|
throw new Error(`Package is not managed by Loadout: ${packageId}`);
|
|
239
|
-
const disabledAgents =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
271
|
+
const disabledAgents = [
|
|
272
|
+
...new Set((state.activations ?? [])
|
|
273
|
+
.filter((activation) => activation.packageId === packageId &&
|
|
274
|
+
activation.installationState === "installed" &&
|
|
275
|
+
activation.activationState === "disabled")
|
|
276
|
+
.map((activation) => activation.agent)),
|
|
277
|
+
].sort();
|
|
244
278
|
const expectedRecord = JSON.stringify(record);
|
|
245
279
|
const expectedActivations = JSON.stringify((state.activations ?? [])
|
|
246
280
|
.filter((activation) => activation.packageId === packageId)
|
package/docs/USER_TEST_GUIDE.md
CHANGED
|
@@ -211,7 +211,7 @@ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle te
|
|
|
211
211
|
## Troubleshooting and recovery
|
|
212
212
|
|
|
213
213
|
- **`loadout` is not found after installation:** confirm `npm install --global
|
|
214
|
-
loadout-ai@0.5.
|
|
214
|
+
loadout-ai@0.5.5` completed, run `hash -r`, and confirm npm's global binary
|
|
215
215
|
directory is on `PATH`. For a source checkout, run `npm run build` and `npm link`.
|
|
216
216
|
- **A preview asks for `--approve-risk`:** read the reported scripts, domains,
|
|
217
217
|
credentials, binaries, or instruction findings. If you accept that specific plan,
|