loadout-ai 0.4.0 → 0.4.1
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 +25 -0
- package/MASTER_PLAN.md +94 -3
- package/README.md +2 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/core/active-policy.js +172 -38
- package/dist/src/core/active-set.js +13 -4
- package/dist/src/core/install.js +3 -36
- package/dist/src/core/recommend.js +95 -9
- package/dist/src/core/target-occupancy.js +50 -0
- package/docs/USER_TEST_GUIDE.md +17 -5
- package/docs/superpowers/plans/2026-07-20-project-activation-safety.md +469 -0
- package/docs/superpowers/specs/2026-07-20-project-activation-safety-design.md +228 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const SIGNAL_FILES = new Set([
|
|
|
15
15
|
"next.config.mjs",
|
|
16
16
|
"vite.config.ts",
|
|
17
17
|
"playwright.config.ts",
|
|
18
|
+
"SECURITY.md",
|
|
18
19
|
".git",
|
|
19
20
|
]);
|
|
20
21
|
/** Additive machine-readable boundary for every rule-selected recommendation list. */
|
|
@@ -22,6 +23,45 @@ export const RECOMMENDATION_BOUNDARY = Object.freeze({
|
|
|
22
23
|
selectionMethod: "deterministic-project-signal-rules",
|
|
23
24
|
qualityEvidence: "not-established",
|
|
24
25
|
});
|
|
26
|
+
const SIGNAL_LABELS = {
|
|
27
|
+
"javascript/typescript": "TypeScript",
|
|
28
|
+
playwright: "Playwright",
|
|
29
|
+
"node-cli": "Node CLI",
|
|
30
|
+
"npm-package": "npm package",
|
|
31
|
+
release: "release automation",
|
|
32
|
+
mcp: "MCP tooling",
|
|
33
|
+
security: "security policy",
|
|
34
|
+
commander: "Commander",
|
|
35
|
+
zod: "Zod",
|
|
36
|
+
vitest: "Vitest",
|
|
37
|
+
jest: "Jest",
|
|
38
|
+
};
|
|
39
|
+
const SIGNAL_DISPLAY_ORDER = [
|
|
40
|
+
"javascript/typescript",
|
|
41
|
+
"playwright",
|
|
42
|
+
"node-cli",
|
|
43
|
+
"npm-package",
|
|
44
|
+
"release",
|
|
45
|
+
"mcp",
|
|
46
|
+
"security",
|
|
47
|
+
"commander",
|
|
48
|
+
"zod",
|
|
49
|
+
"vitest",
|
|
50
|
+
"jest",
|
|
51
|
+
];
|
|
52
|
+
export function formatDetectedSignals(signals) {
|
|
53
|
+
const values = new Set([
|
|
54
|
+
...signals.languages,
|
|
55
|
+
...signals.frameworks,
|
|
56
|
+
...signals.roles,
|
|
57
|
+
...signals.tools,
|
|
58
|
+
]);
|
|
59
|
+
const ordered = [
|
|
60
|
+
...SIGNAL_DISPLAY_ORDER.filter((value) => values.delete(value)),
|
|
61
|
+
...values,
|
|
62
|
+
];
|
|
63
|
+
return ordered.map((value) => SIGNAL_LABELS[value] ?? value).join(", ");
|
|
64
|
+
}
|
|
25
65
|
export async function scanProject(root = process.cwd()) {
|
|
26
66
|
const absolute = resolve(root);
|
|
27
67
|
const entries = await readdir(absolute, { withFileTypes: true });
|
|
@@ -31,6 +71,8 @@ export async function scanProject(root = process.cwd()) {
|
|
|
31
71
|
.sort();
|
|
32
72
|
const languages = new Set();
|
|
33
73
|
const frameworks = new Set();
|
|
74
|
+
const roles = new Set();
|
|
75
|
+
const tools = new Set();
|
|
34
76
|
if (files.includes("package.json")) {
|
|
35
77
|
languages.add("javascript/typescript");
|
|
36
78
|
try {
|
|
@@ -44,8 +86,28 @@ export async function scanProject(root = process.cwd()) {
|
|
|
44
86
|
frameworks.add("vue");
|
|
45
87
|
if (deps.svelte)
|
|
46
88
|
frameworks.add("svelte");
|
|
47
|
-
if (deps.playwright || deps["@playwright/test"])
|
|
89
|
+
if (deps.playwright || deps["@playwright/test"]) {
|
|
48
90
|
frameworks.add("playwright");
|
|
91
|
+
tools.add("playwright");
|
|
92
|
+
}
|
|
93
|
+
if (deps.vitest)
|
|
94
|
+
tools.add("vitest");
|
|
95
|
+
if (deps.jest)
|
|
96
|
+
tools.add("jest");
|
|
97
|
+
if (deps.commander)
|
|
98
|
+
tools.add("commander");
|
|
99
|
+
if (deps.zod)
|
|
100
|
+
tools.add("zod");
|
|
101
|
+
if (pkg.bin)
|
|
102
|
+
roles.add("node-cli");
|
|
103
|
+
if (pkg.publishConfig || pkg.private === false)
|
|
104
|
+
roles.add("npm-package");
|
|
105
|
+
const scripts = pkg.scripts ?? {};
|
|
106
|
+
if (scripts.prepack ||
|
|
107
|
+
Object.keys(scripts).some((name) => /(?:package|release)/.test(name)))
|
|
108
|
+
roles.add("release");
|
|
109
|
+
if ((pkg.keywords ?? []).some((keyword) => /(?:^|-)mcp(?:-|$)/i.test(keyword)))
|
|
110
|
+
roles.add("mcp");
|
|
49
111
|
}
|
|
50
112
|
catch {
|
|
51
113
|
/* malformed project metadata is reported through an empty framework set */
|
|
@@ -65,22 +127,37 @@ export async function scanProject(root = process.cwd()) {
|
|
|
65
127
|
languages.add(".net");
|
|
66
128
|
if (files.some((file) => file.startsWith("next.config")))
|
|
67
129
|
frameworks.add("next.js");
|
|
68
|
-
if (files.includes("playwright.config.ts"))
|
|
130
|
+
if (files.includes("playwright.config.ts")) {
|
|
69
131
|
frameworks.add("playwright");
|
|
132
|
+
tools.add("playwright");
|
|
133
|
+
}
|
|
134
|
+
if (files.includes("SECURITY.md"))
|
|
135
|
+
roles.add("security");
|
|
70
136
|
return {
|
|
71
137
|
root: absolute,
|
|
72
138
|
languages: [...languages],
|
|
73
139
|
frameworks: [...frameworks],
|
|
140
|
+
roles: [...roles],
|
|
141
|
+
tools: [...tools],
|
|
74
142
|
files,
|
|
75
143
|
};
|
|
76
144
|
}
|
|
77
145
|
export function recommendPackages(signals, catalog) {
|
|
78
|
-
const
|
|
146
|
+
const packages = new Map(catalog.map((pkg) => [pkg.id, pkg]));
|
|
79
147
|
const result = [];
|
|
80
148
|
const add = (packageId, reason, confidence) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
result.push({
|
|
149
|
+
const pkg = packages.get(packageId);
|
|
150
|
+
if (pkg && !result.some((item) => item.packageId === packageId))
|
|
151
|
+
result.push({
|
|
152
|
+
packageId,
|
|
153
|
+
reason,
|
|
154
|
+
confidence,
|
|
155
|
+
kind: pkg.components?.includes("skill")
|
|
156
|
+
? "skill-library"
|
|
157
|
+
: pkg.components?.some((component) => component === "mcp" || component === "plugin")
|
|
158
|
+
? "mcp-runtime"
|
|
159
|
+
: "unavailable",
|
|
160
|
+
});
|
|
84
161
|
};
|
|
85
162
|
add("superpowers", "Useful engineering planning, testing, and review workflows for most repositories.", "high");
|
|
86
163
|
add("context7", "Current library documentation helps agents avoid outdated APIs.", signals.languages.length ? "high" : "medium");
|
|
@@ -129,6 +206,7 @@ export function personalizeRecommendations(recommendations, signals, outcomes, a
|
|
|
129
206
|
packageId: item.packageId,
|
|
130
207
|
reason: item.reason,
|
|
131
208
|
confidence: item.confidence,
|
|
209
|
+
kind: item.kind,
|
|
132
210
|
...(item.localOutcomeAdjustment !== undefined
|
|
133
211
|
? { localOutcomeAdjustment: item.localOutcomeAdjustment }
|
|
134
212
|
: {}),
|
|
@@ -174,14 +252,22 @@ export function profileManifestPackages(profile, catalog) {
|
|
|
174
252
|
export function formatRecommendations(signals, recommendations) {
|
|
175
253
|
const lines = [
|
|
176
254
|
`Project: ${basename(signals.root)}`,
|
|
177
|
-
`Detected: ${
|
|
255
|
+
`Detected: ${formatDetectedSignals(signals) || "no known project signals"}`,
|
|
178
256
|
"",
|
|
179
257
|
"Rule-based project suggestions:",
|
|
180
258
|
"Rules use detected project signals and catalog membership; they do not prove package quality.",
|
|
181
259
|
];
|
|
182
260
|
if (!recommendations.length)
|
|
183
261
|
lines.push(" No matching catalog packages found.");
|
|
184
|
-
|
|
185
|
-
|
|
262
|
+
const kindLabels = {
|
|
263
|
+
"skill-library": "skill library",
|
|
264
|
+
"mcp-runtime": "MCP/runtime setup",
|
|
265
|
+
unavailable: "unavailable",
|
|
266
|
+
};
|
|
267
|
+
for (const item of recommendations) {
|
|
268
|
+
lines.push(` ${item.packageId} [${item.confidence}, ${kindLabels[item.kind]}] — ${item.reason}`);
|
|
269
|
+
if (item.kind === "mcp-runtime")
|
|
270
|
+
lines.push(" Explicit setup only; preview credentials and permissions before enabling it.");
|
|
271
|
+
}
|
|
186
272
|
return lines.join("\n");
|
|
187
273
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Inspect a prospective skill target without following symlinks or executing
|
|
5
|
+
* any content. Missing paths and recursively empty directories are safe to
|
|
6
|
+
* replace; every uncertain state fails closed.
|
|
7
|
+
*/
|
|
8
|
+
export async function inspectTargetOccupancy(path, maximumEntries = 10_000) {
|
|
9
|
+
let root;
|
|
10
|
+
try {
|
|
11
|
+
root = await lstat(path);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error &&
|
|
15
|
+
typeof error === "object" &&
|
|
16
|
+
"code" in error &&
|
|
17
|
+
error.code === "ENOENT")
|
|
18
|
+
return { occupied: false };
|
|
19
|
+
return { occupied: true, reason: "unreadable" };
|
|
20
|
+
}
|
|
21
|
+
if (root.isSymbolicLink())
|
|
22
|
+
return { occupied: true, reason: "symlink" };
|
|
23
|
+
if (!root.isDirectory())
|
|
24
|
+
return { occupied: true, reason: "unsupported" };
|
|
25
|
+
const queue = [path];
|
|
26
|
+
let inspected = 0;
|
|
27
|
+
while (queue.length) {
|
|
28
|
+
const directory = queue.pop();
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { occupied: true, reason: "unreadable" };
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
inspected += 1;
|
|
38
|
+
if (inspected > maximumEntries)
|
|
39
|
+
return { occupied: true, reason: "inspection-limit" };
|
|
40
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
41
|
+
queue.push(join(directory, entry.name));
|
|
42
|
+
else
|
|
43
|
+
return {
|
|
44
|
+
occupied: true,
|
|
45
|
+
reason: entry.isSymbolicLink() ? "symlink" : "content",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { occupied: false };
|
|
50
|
+
}
|
package/docs/USER_TEST_GUIDE.md
CHANGED
|
@@ -26,15 +26,19 @@ Loadout-managed skills from your own pre-existing skills. `health` checks local
|
|
|
26
26
|
loadout catalog --json
|
|
27
27
|
loadout candidate list --limit 10
|
|
28
28
|
loadout recommend --project .
|
|
29
|
-
loadout optimize --project .
|
|
29
|
+
loadout optimize --project . --agents codex,claude-code --limit 30
|
|
30
30
|
loadout tool
|
|
31
31
|
loadout tool graphify
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run the project commands from the project you care about, or replace `.` with its
|
|
35
|
-
absolute path. `
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
absolute path. `recommend` labels ordinary skill libraries separately from MCP or
|
|
36
|
+
runtime integrations that require explicit setup. `optimize` is still a preview
|
|
37
|
+
until `--yes` is supplied. Its limit applies separately to every agent and includes
|
|
38
|
+
both Loadout-managed and pre-existing unmanaged skills, so Claude and Codex can
|
|
39
|
+
receive different numbers of additions. `tool graphify` is also a preview; Graphify
|
|
40
|
+
is a reviewed runtime tool and does not need an OpenAI or Anthropic API key for its
|
|
41
|
+
code-only install.
|
|
38
42
|
|
|
39
43
|
## 3. Open the optional dashboard
|
|
40
44
|
|
|
@@ -64,6 +68,10 @@ loadout setup --mode power --agents codex,claude-code
|
|
|
64
68
|
loadout setup --mode maximum --agents codex,claude-code
|
|
65
69
|
```
|
|
66
70
|
|
|
71
|
+
Maximum stores reviewed copies in Loadout's disabled library; it does not expose the
|
|
72
|
+
whole catalog to each agent. Follow it with `loadout optimize --project .
|
|
73
|
+
--agents codex,claude-code --limit 30` to preview a compact project-aware working set.
|
|
74
|
+
|
|
67
75
|
At the API-access question, choose `None` unless you separately pay for a
|
|
68
76
|
provider API. A ChatGPT Plus or Claude Pro subscription is not an API key. Core
|
|
69
77
|
skill profiles do not require one; credentialed MCP and runtime operations stay
|
|
@@ -161,7 +169,7 @@ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle te
|
|
|
161
169
|
## Troubleshooting and recovery
|
|
162
170
|
|
|
163
171
|
- **`loadout` is not found after installation:** confirm `npm install --global
|
|
164
|
-
loadout-ai@0.4.
|
|
172
|
+
loadout-ai@0.4.1` completed, run `hash -r`, and confirm npm's global binary
|
|
165
173
|
directory is on `PATH`. For a source checkout, run `npm run build` and `npm link`.
|
|
166
174
|
- **A preview asks for `--approve-risk`:** read the reported scripts, domains,
|
|
167
175
|
credentials, binaries, or instruction findings. If you accept that specific plan,
|
|
@@ -172,6 +180,10 @@ loadout-ai@0.4.0` completed, run `hash -r`, and confirm npm's global binary
|
|
|
172
180
|
legacy snapshot without post-mutation evidence. Run `loadout health --explain` and
|
|
173
181
|
inspect the affected path before deciding whether an explicit force option is
|
|
174
182
|
appropriate; do not delete the path merely to make the command pass.
|
|
183
|
+
- **Activation reports fewer additions for one agent:** this is expected when that
|
|
184
|
+
agent already has unmanaged or managed skills. `--limit` is a total per-agent
|
|
185
|
+
ceiling, not a request to add that many new skills. Recursively empty rollback
|
|
186
|
+
directories do not consume capacity and are safe for Loadout to reuse.
|
|
175
187
|
- **A fetch, discovery, or update check fails:** retry only after checking network,
|
|
176
188
|
proxy, DNS, and source-host access. Local inventory, library, health, rollback, and
|
|
177
189
|
offline fixture tests remain separate; an unavailable live check is not a pass.
|