loadout-ai 0.5.2 → 0.5.3
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 +13 -0
- package/MASTER_PLAN.md +9 -0
- package/README.md +2 -2
- package/dist/src/cli.js +3 -3
- package/dist/src/core/health.js +34 -6
- package/dist/src/core/mcp-recipes.js +1 -1
- package/dist/src/core/runtime-tools.js +29 -7
- package/dist/src/core/skill-inventory.js +24 -8
- package/docs/USER_TEST_GUIDE.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.3 - 2026-07-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Include reviewed runtime-tool skills installed at official host-specific paths in
|
|
10
|
+
inventory, provenance, capacity, and health reporting. Graphify is now recognized
|
|
11
|
+
as Loadout-managed in both `~/.claude/skills` and Codex's `~/.codex/skills` target.
|
|
12
|
+
- Report managed MCP server and runtime-tool counts in health, and warn when an
|
|
13
|
+
installed runtime tool's expected skill target disappears.
|
|
14
|
+
- Give runtime-tool install snapshots meaningful labels instead of `managed change`.
|
|
15
|
+
- Allow 30 seconds by default for a cold reviewed MCP connection handshake and make
|
|
16
|
+
MCP-only removal preview language accurately describe a removal plan.
|
|
17
|
+
|
|
5
18
|
## 0.5.2 - 2026-07-21
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/MASTER_PLAN.md
CHANGED
|
@@ -285,6 +285,15 @@ merged `codex/relatable-readme-hero` remote branch remains safe to delete after
|
|
|
285
285
|
counts. CLI-only TypeScript projects also reject browser-testing and
|
|
286
286
|
Playwright false positives, and the default preview removes unexplained scores
|
|
287
287
|
and duplicated delta output. Release as `0.5.2` before resuming activation.
|
|
288
|
+
- [x] `P18-39 [TERRA]` Integrate reviewed runtime tools into the same inventory and
|
|
289
|
+
health truth as skill repositories. Founder testing proved Graphify installed
|
|
290
|
+
correctly for Claude Code and Codex, but the scanner counted Claude's target as
|
|
291
|
+
unmanaged and omitted Codex's official `~/.codex/skills` target because its
|
|
292
|
+
standard collection root is `~/.agents/skills`. Scan every registered runtime
|
|
293
|
+
target, attribute it to `runtime-tool:<id>`, count both host targets in health,
|
|
294
|
+
surface runtime/MCP totals and missing targets, label runtime snapshots, and
|
|
295
|
+
raise the default cold MCP handshake window to the tested 30 seconds. Release
|
|
296
|
+
as `0.5.3` before continuing Graphify removal and discovery/update acceptance.
|
|
288
297
|
|
|
289
298
|
### Release 0.3 lifecycle hardening
|
|
290
299
|
|
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.3`; 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.3
|
|
80
80
|
loadout --version
|
|
81
81
|
loadout guide
|
|
82
82
|
```
|
package/dist/src/cli.js
CHANGED
|
@@ -263,7 +263,7 @@ async function runSetup(options) {
|
|
|
263
263
|
reader?.close();
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
const LOADOUT_VERSION = "0.5.
|
|
266
|
+
const LOADOUT_VERSION = "0.5.3";
|
|
267
267
|
function durableSchedulerLauncher() {
|
|
268
268
|
return [
|
|
269
269
|
join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
|
|
@@ -1007,7 +1007,7 @@ program
|
|
|
1007
1007
|
const plan = await planRemove(packageId);
|
|
1008
1008
|
console.log(JSON.stringify(plan, null, 2));
|
|
1009
1009
|
if (!options.yes)
|
|
1010
|
-
return console.log("Dry run only. Re-run with --yes to
|
|
1010
|
+
return console.log("Dry run only. Re-run with --yes to apply this removal plan.");
|
|
1011
1011
|
const snapshot = await applyRemove(plan, { force: options.force });
|
|
1012
1012
|
console.log(`Removed ${packageId}. Snapshot: ${snapshot}`);
|
|
1013
1013
|
});
|
|
@@ -2142,7 +2142,7 @@ program
|
|
|
2142
2142
|
.option("--connect", "launch the exact pinned artifact and perform an MCP initialize handshake")
|
|
2143
2143
|
.option("--credential <mapping>", "credential mapping NAME=env:VARIABLE or NAME=keychain:SERVICE (repeatable)", collectOption, [])
|
|
2144
2144
|
.option("--credential-account <account>", "account for keychain mappings")
|
|
2145
|
-
.option("--timeout <milliseconds>", "real connection timeout", "
|
|
2145
|
+
.option("--timeout <milliseconds>", "real connection timeout", "30000")
|
|
2146
2146
|
.option("--approve-risk", "approve launching the reviewed pinned MCP artifact for --connect")
|
|
2147
2147
|
.option("--json", "emit machine-readable JSON")
|
|
2148
2148
|
.option("--no-key", "list recipes needing no separately billed AI/model API key")
|
package/dist/src/core/health.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { managedFileReadPath } from "./active-set.js";
|
|
4
5
|
import { detectAgents } from "./paths.js";
|
|
5
6
|
import { readInstallState } from "./state.js";
|
|
6
7
|
import { buildUpdatePlan } from "./update.js";
|
|
7
8
|
import { codexMcpServerFingerprint } from "./codex-mcp.js";
|
|
9
|
+
import { listInstalledRuntimeToolSkillTargets, listInstalledRuntimeTools, } from "./runtime-tools.js";
|
|
8
10
|
async function drift(record, activations) {
|
|
9
11
|
const changed = [];
|
|
10
12
|
for (const file of record.files) {
|
|
@@ -35,18 +37,30 @@ async function mcpDrift(configPath, serverName, expected, configFormat = "json")
|
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
export async function buildHealthReport(options = {}) {
|
|
38
|
-
const [agents, state, updates] = await Promise.all([
|
|
39
|
-
detectAgents(),
|
|
40
|
+
const [agents, state, updates, runtimeTools, runtimeTargets] = await Promise.all([
|
|
41
|
+
options.agents ? options.agents() : detectAgents(),
|
|
40
42
|
readInstallState(),
|
|
41
43
|
options.updates
|
|
42
44
|
? options.updates()
|
|
43
45
|
: options.checkUpdates
|
|
44
46
|
? buildUpdatePlan()
|
|
45
47
|
: Promise.resolve([]),
|
|
48
|
+
listInstalledRuntimeTools(),
|
|
49
|
+
listInstalledRuntimeToolSkillTargets(),
|
|
46
50
|
]);
|
|
47
51
|
const drifted = (await Promise.all(state.installs.map((record) => drift(record, state.activations ?? [])))).flat();
|
|
48
52
|
const driftedMcpServers = (await Promise.all((state.mcpInstalls ?? []).map((entry) => mcpDrift(entry.configPath, entry.serverName, entry.fingerprint, entry.configFormat)))).filter(Boolean).length;
|
|
49
53
|
const findings = [];
|
|
54
|
+
const runtimeTargetPresence = await Promise.all(runtimeTargets.map(async (target) => {
|
|
55
|
+
try {
|
|
56
|
+
await readFile(join(target.path, "SKILL.md"));
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
const missingRuntimeTargets = runtimeTargetPresence.filter((present) => !present).length;
|
|
50
64
|
if (!agents.some((agent) => agent.installed))
|
|
51
65
|
findings.push({
|
|
52
66
|
level: "error",
|
|
@@ -81,6 +95,13 @@ export async function buildHealthReport(options = {}) {
|
|
|
81
95
|
message: `${driftedMcpServers} managed MCP server entry or entries changed or disappeared outside Loadout.`,
|
|
82
96
|
fix: "Review the MCP config, then synchronize or remove the owning package.",
|
|
83
97
|
});
|
|
98
|
+
if (missingRuntimeTargets)
|
|
99
|
+
findings.push({
|
|
100
|
+
level: "warning",
|
|
101
|
+
code: "managed-runtime-target-drift",
|
|
102
|
+
message: `${missingRuntimeTargets} managed runtime-tool skill target(s) changed or disappeared.`,
|
|
103
|
+
fix: "Review the target, then remove and reinstall the owning runtime tool.",
|
|
104
|
+
});
|
|
84
105
|
const available = updates.filter((update) => update.status === "update-available");
|
|
85
106
|
if (available.length)
|
|
86
107
|
findings.push({
|
|
@@ -99,9 +120,11 @@ export async function buildHealthReport(options = {}) {
|
|
|
99
120
|
message: `${errors.length} update check(s) could not be completed.`,
|
|
100
121
|
fix: "Check connectivity and retry.",
|
|
101
122
|
});
|
|
102
|
-
const configured = state.installs.length > 0 ||
|
|
123
|
+
const configured = state.installs.length > 0 ||
|
|
124
|
+
(state.mcpInstalls ?? []).length > 0 ||
|
|
125
|
+
runtimeTools.length > 0;
|
|
103
126
|
const activeSkills = (state.activations ?? []).filter((entry) => entry.installationState === "installed" &&
|
|
104
|
-
entry.activationState === "active").length;
|
|
127
|
+
entry.activationState === "active").length + runtimeTargetPresence.filter(Boolean).length;
|
|
105
128
|
const disabledSkills = (state.activations ?? []).filter((entry) => entry.installationState === "installed" &&
|
|
106
129
|
entry.activationState === "disabled").length;
|
|
107
130
|
const status = findings.some((finding) => finding.level === "error")
|
|
@@ -110,7 +133,10 @@ export async function buildHealthReport(options = {}) {
|
|
|
110
133
|
? "not-configured"
|
|
111
134
|
: findings.some((finding) => finding.level === "warning")
|
|
112
135
|
? "attention"
|
|
113
|
-
: activeSkills === 0 &&
|
|
136
|
+
: activeSkills === 0 &&
|
|
137
|
+
disabledSkills > 0 &&
|
|
138
|
+
(state.mcpInstalls ?? []).length === 0 &&
|
|
139
|
+
runtimeTools.length === 0
|
|
114
140
|
? "library-only"
|
|
115
141
|
: "healthy";
|
|
116
142
|
return {
|
|
@@ -120,6 +146,8 @@ export async function buildHealthReport(options = {}) {
|
|
|
120
146
|
installedPackages: state.installs.length,
|
|
121
147
|
activeSkills,
|
|
122
148
|
disabledSkills,
|
|
149
|
+
managedMcpServers: (state.mcpInstalls ?? []).length,
|
|
150
|
+
managedRuntimeTools: runtimeTools.length,
|
|
123
151
|
updatesChecked: Boolean(options.updates || options.checkUpdates),
|
|
124
152
|
updatesAvailable: available.length,
|
|
125
153
|
driftedFiles: drifted.length,
|
|
@@ -137,7 +165,7 @@ export function formatHealthReport(report) {
|
|
|
137
165
|
: "✗";
|
|
138
166
|
const lines = [
|
|
139
167
|
`${icon} Loadout health: ${report.status === "not-configured" ? "not configured" : report.status === "library-only" ? "library ready (nothing active)" : report.status}`,
|
|
140
|
-
`Packages: ${report.installedPackages} managed; skills: ${report.activeSkills ?? 0} active, ${report.disabledSkills ?? 0} disabled; ${report.updatesChecked ? `${report.updatesAvailable} update(s)` : "updates not checked (use --updates)"}; ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
|
|
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)`,
|
|
141
169
|
];
|
|
142
170
|
for (const finding of report.findings)
|
|
143
171
|
lines.push(`${finding.level === "ok" ? "✓" : finding.level === "error" ? "✗" : finding.level === "warning" ? "!" : "•"} ${finding.message}${finding.fix ? ` ${finding.fix}` : ""}`);
|
|
@@ -148,7 +148,7 @@ export async function planMcpRecipe(recipeId, configPath, options = {}) {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
151
|
-
const DEFAULT_CONNECTION_TIMEOUT_MS =
|
|
151
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 30_000;
|
|
152
152
|
const DEFAULT_CONNECTION_OUTPUT_BYTES = 256 * 1024;
|
|
153
153
|
const INITIALIZE_ID = "loadout-initialize";
|
|
154
154
|
function validateConnectionBounds(timeoutMs, maxOutputBytes) {
|
|
@@ -222,6 +222,28 @@ export async function listInstalledRuntimeTools(stateHome = loadoutHome()) {
|
|
|
222
222
|
const state = await readState(stateHome);
|
|
223
223
|
return Object.keys(state.tools).sort();
|
|
224
224
|
}
|
|
225
|
+
/** Return agent skill targets owned by installed reviewed runtime tools. */
|
|
226
|
+
export async function listInstalledRuntimeToolSkillTargets(options = {}) {
|
|
227
|
+
const state = await readState(options.stateHome ?? loadoutHome());
|
|
228
|
+
const home = options.home ?? userHome();
|
|
229
|
+
const platform = currentRecipePlatform(options.platform);
|
|
230
|
+
return Object.entries(state.tools).flatMap(([toolId, installed]) => {
|
|
231
|
+
const recipe = findRuntimeToolRecipe(toolId);
|
|
232
|
+
return installed.agents.flatMap((agent) => {
|
|
233
|
+
const target = recipe.targets[agent];
|
|
234
|
+
return target
|
|
235
|
+
? [
|
|
236
|
+
{
|
|
237
|
+
toolId,
|
|
238
|
+
packageId: `runtime-tool:${toolId}`,
|
|
239
|
+
agent,
|
|
240
|
+
path: resolveRuntimeRecipePath(home, target.path, platform),
|
|
241
|
+
},
|
|
242
|
+
]
|
|
243
|
+
: [];
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
225
247
|
async function writeState(state, stateHome) {
|
|
226
248
|
await writeFileAtomically(statePath(stateHome), `${JSON.stringify(state, null, 2)}\n`);
|
|
227
249
|
}
|
|
@@ -231,12 +253,10 @@ export function findRuntimeToolRecipe(id) {
|
|
|
231
253
|
throw new Error(`Unknown runtime tool '${id}'. Available: ${REVIEWED_RUNTIME_TOOLS.map((item) => item.id).join(", ")}`);
|
|
232
254
|
return recipe;
|
|
233
255
|
}
|
|
234
|
-
function currentRecipePlatform() {
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return process.platform;
|
|
239
|
-
throw new Error(`Runtime tool recipes do not support ${process.platform}`);
|
|
256
|
+
function currentRecipePlatform(platform = process.platform) {
|
|
257
|
+
if (platform === "darwin" || platform === "linux" || platform === "win32")
|
|
258
|
+
return platform;
|
|
259
|
+
throw new Error(`Runtime tool recipes do not support ${platform}`);
|
|
240
260
|
}
|
|
241
261
|
function buildRuntimeToolCommands(recipe, runtimeRoot, agents, platform, action) {
|
|
242
262
|
if (action === "remove")
|
|
@@ -425,7 +445,9 @@ export async function applyRuntimeToolPlan(plan, options) {
|
|
|
425
445
|
const snapshotRoots = plan.recipe.snapshotRoots.flatMap((root) => root === "{runtimeRoot}"
|
|
426
446
|
? [plan.runtimeRoot]
|
|
427
447
|
: plan.agents.map((agent) => agent.target));
|
|
428
|
-
snapshot = await createSnapshot(snapshotRoots
|
|
448
|
+
snapshot = await createSnapshot(snapshotRoots, {
|
|
449
|
+
label: `install runtime tool ${plan.recipe.displayName}`,
|
|
450
|
+
});
|
|
429
451
|
const runner = options.runner ?? defaultRunner;
|
|
430
452
|
const env = runtimeEnvironment(plan);
|
|
431
453
|
const healthCheckStart = plan.commands.length - plan.recipe.healthChecks.length;
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { readInstallState } from "./state.js";
|
|
5
|
+
import { listInstalledRuntimeToolSkillTargets, } from "./runtime-tools.js";
|
|
5
6
|
const MAX_SCAN_DEPTH = 6;
|
|
6
7
|
export const RECOMMENDED_ACTIVE_SKILLS = 30;
|
|
7
8
|
function cleanFrontmatterValue(value) {
|
|
@@ -17,7 +18,10 @@ function isInside(root, candidate) {
|
|
|
17
18
|
const path = relative(resolve(root), resolve(candidate));
|
|
18
19
|
return path === "" || (!path.startsWith("..") && !path.startsWith(sep));
|
|
19
20
|
}
|
|
20
|
-
function owningPackage(skillRoot, agent, records, activations) {
|
|
21
|
+
function owningPackage(skillRoot, agent, records, activations, runtimeTargets) {
|
|
22
|
+
const runtime = runtimeTargets.find((target) => target.agent === agent && isInside(target.path, skillRoot));
|
|
23
|
+
if (runtime)
|
|
24
|
+
return runtime.packageId;
|
|
21
25
|
const active = activations.find((record) => record.agent === agent &&
|
|
22
26
|
record.activationState === "active" &&
|
|
23
27
|
record.installationState === "installed" &&
|
|
@@ -29,8 +33,15 @@ function owningPackage(skillRoot, agent, records, activations) {
|
|
|
29
33
|
activation.agent === agent) &&
|
|
30
34
|
record.files.some((file) => isInside(skillRoot, file.path)))?.packageId;
|
|
31
35
|
}
|
|
32
|
-
async function scanAgentSkills(agent, records, activations) {
|
|
33
|
-
const
|
|
36
|
+
async function scanAgentSkills(agent, records, activations, runtimeTargets) {
|
|
37
|
+
const primaryRoot = resolve(agent.skillsDirectory);
|
|
38
|
+
const roots = [
|
|
39
|
+
primaryRoot,
|
|
40
|
+
...runtimeTargets
|
|
41
|
+
.filter((target) => target.agent === agent.id)
|
|
42
|
+
.map((target) => resolve(target.path))
|
|
43
|
+
.filter((target) => !isInside(primaryRoot, target)),
|
|
44
|
+
];
|
|
34
45
|
const skills = [];
|
|
35
46
|
const warnings = [];
|
|
36
47
|
async function visit(directory, depth) {
|
|
@@ -60,7 +71,7 @@ async function scanAgentSkills(agent, records, activations) {
|
|
|
60
71
|
try {
|
|
61
72
|
const path = join(directory, "SKILL.md");
|
|
62
73
|
const content = await readFile(path, "utf8");
|
|
63
|
-
const packageId = owningPackage(directory, agent.id, records, activations);
|
|
74
|
+
const packageId = owningPackage(directory, agent.id, records, activations, runtimeTargets);
|
|
64
75
|
const sourceHints = [
|
|
65
76
|
...new Set([
|
|
66
77
|
...content.matchAll(/https?:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]*[A-Za-z0-9_-])/g),
|
|
@@ -102,7 +113,8 @@ async function scanAgentSkills(agent, records, activations) {
|
|
|
102
113
|
await visit(join(directory, child.name), depth + 1);
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
|
-
|
|
116
|
+
for (const root of [...new Set(roots)])
|
|
117
|
+
await visit(root, 0);
|
|
106
118
|
return {
|
|
107
119
|
skills: skills.sort((left, right) => left.path.localeCompare(right.path)),
|
|
108
120
|
warnings,
|
|
@@ -141,8 +153,11 @@ function duplicateGroups(skills) {
|
|
|
141
153
|
* safety verdict and no files are changed by this scan.
|
|
142
154
|
*/
|
|
143
155
|
export async function scanInstalledSkills(agents) {
|
|
144
|
-
const state = await
|
|
145
|
-
|
|
156
|
+
const [state, runtimeTargets] = await Promise.all([
|
|
157
|
+
readInstallState(),
|
|
158
|
+
listInstalledRuntimeToolSkillTargets(),
|
|
159
|
+
]);
|
|
160
|
+
const scans = await Promise.all(agents.map((agent) => scanAgentSkills(agent, state.installs, state.activations ?? [], runtimeTargets)));
|
|
146
161
|
const skills = scans.flatMap((scan) => scan.skills);
|
|
147
162
|
const warnings = scans.flatMap((scan) => scan.warnings);
|
|
148
163
|
const summaries = agents.map((agent, index) => {
|
|
@@ -157,6 +172,7 @@ export async function scanInstalledSkills(agents) {
|
|
|
157
172
|
managed,
|
|
158
173
|
unmanaged: entries.length - managed,
|
|
159
174
|
overRecommendedLimit: entries.length > RECOMMENDED_ACTIVE_SKILLS,
|
|
175
|
+
runtimeToolTargets: runtimeTargets.filter((target) => target.agent === agent.id).length,
|
|
160
176
|
};
|
|
161
177
|
});
|
|
162
178
|
for (const summary of summaries.filter((item) => item.overRecommendedLimit))
|
|
@@ -185,7 +201,7 @@ export function formatInstalledSkillInventory(report) {
|
|
|
185
201
|
"",
|
|
186
202
|
];
|
|
187
203
|
for (const agent of report.agents)
|
|
188
|
-
lines.push(`${agent.detected ? "✓" : "○"} ${agent.displayName}: ${agent.total} skill(s) (${agent.managed} managed, ${agent.unmanaged} unmanaged) — ${agent.directory}`);
|
|
204
|
+
lines.push(`${agent.detected ? "✓" : "○"} ${agent.displayName}: ${agent.total} skill(s) (${agent.managed} managed, ${agent.unmanaged} unmanaged)${agent.runtimeToolTargets ? `, including ${agent.runtimeToolTargets} runtime-tool skill(s)` : ""} — ${agent.directory}`);
|
|
189
205
|
for (const warning of report.warnings)
|
|
190
206
|
lines.push(`! ${warning}`);
|
|
191
207
|
lines.push("", "Read-only scan complete. Unmanaged content was not changed or judged automatically.");
|
package/docs/USER_TEST_GUIDE.md
CHANGED
|
@@ -183,7 +183,7 @@ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle te
|
|
|
183
183
|
## Troubleshooting and recovery
|
|
184
184
|
|
|
185
185
|
- **`loadout` is not found after installation:** confirm `npm install --global
|
|
186
|
-
loadout-ai@0.5.
|
|
186
|
+
loadout-ai@0.5.3` completed, run `hash -r`, and confirm npm's global binary
|
|
187
187
|
directory is on `PATH`. For a source checkout, run `npm run build` and `npm link`.
|
|
188
188
|
- **A preview asks for `--approve-risk`:** read the reported scripts, domains,
|
|
189
189
|
credentials, binaries, or instruction findings. If you accept that specific plan,
|