loadout-ai 0.9.3 → 0.9.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 +27 -0
- package/README.md +43 -29
- package/catalog/discovered.json +78352 -41856
- package/dist/src/core/catalog/skills.js +61 -3
- package/dist/src/core/coordination/lock.js +25 -3
- package/dist/src/core/reporting/cli-guide.js +4 -2
- package/dist/src/core/workspace/active-policy.js +1 -1
- package/docs/DISCOVERED.md +249 -249
- package/docs/REFERENCE.md +6 -2
- package/docs/USER_TEST_GUIDE.md +4 -3
- package/docs/superpowers/plans/2026-09-08-fast-launch-demo.md +65 -0
- package/docs/superpowers/plans/2026-09-08-maximum-curator-demo-recording.md +247 -0
- package/docs/superpowers/specs/2026-09-08-fast-launch-demo-design.md +42 -0
- package/docs/superpowers/specs/2026-09-08-maximum-curator-demo-design.md +73 -0
- package/docs/superpowers/specs/2026-09-08-social-launch-copy-design.md +43 -0
- package/package.json +4 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { cp, lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { cp, lstat, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { ensureDirectory } from "../agents/paths.js";
|
|
4
5
|
import { assertSkillSecurity, scanSkillSecurity } from "./skill-security.js";
|
|
5
6
|
export async function discoverSkillDirectories(root, options = {}) {
|
|
@@ -82,6 +83,60 @@ export async function discoverSkillDirectories(root, options = {}) {
|
|
|
82
83
|
await visit(root, 0);
|
|
83
84
|
return result;
|
|
84
85
|
}
|
|
86
|
+
// A link such as `references/security-checklist.md`, not a deeper path like
|
|
87
|
+
// `docs/references/x.md`, which the lookbehind leaves to its own directory.
|
|
88
|
+
const SHARED_REFERENCE = /(?<![\w./-])references\/([A-Za-z0-9][\w.-]*\.md)\b/g;
|
|
89
|
+
async function isRegularFile(path) {
|
|
90
|
+
try {
|
|
91
|
+
const info = await lstat(path);
|
|
92
|
+
return info.isFile() && !info.isSymbolicLink();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Some repositories keep shared checklists in a root `references/` folder that
|
|
100
|
+
* their skills link to relatively. A skill copied on its own would point at
|
|
101
|
+
* nothing, so plan from a staged copy that carries those files beside it.
|
|
102
|
+
*/
|
|
103
|
+
async function withSharedReferences(skill, packageRoot) {
|
|
104
|
+
if (skill === packageRoot)
|
|
105
|
+
return skill;
|
|
106
|
+
const text = await readFile(join(skill, "SKILL.md"), "utf8");
|
|
107
|
+
const names = new Set([...text.matchAll(SHARED_REFERENCE)].map((m) => m[1]));
|
|
108
|
+
const sharedRoot = join(packageRoot, "references");
|
|
109
|
+
// lstat checks only the last path segment, so a symlinked folder would let a
|
|
110
|
+
// package copy files from anywhere on disk into the skill.
|
|
111
|
+
const rootInfo = await lstat(sharedRoot).catch(() => undefined);
|
|
112
|
+
if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink())
|
|
113
|
+
return skill;
|
|
114
|
+
const shared = [];
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
if (!name || (await isRegularFile(join(skill, "references", name))))
|
|
117
|
+
continue;
|
|
118
|
+
const candidate = join(sharedRoot, name);
|
|
119
|
+
if (await isRegularFile(candidate))
|
|
120
|
+
shared.push(candidate);
|
|
121
|
+
}
|
|
122
|
+
if (!shared.length)
|
|
123
|
+
return skill;
|
|
124
|
+
// ponytail: staged copies stay in the OS temp dir until it is cleaned; track and remove them if installs become frequent
|
|
125
|
+
const staged = join(await mkdtemp(join(tmpdir(), "loadout-skill-")), basename(skill));
|
|
126
|
+
await cp(skill, staged, {
|
|
127
|
+
recursive: true,
|
|
128
|
+
errorOnExist: true,
|
|
129
|
+
force: false,
|
|
130
|
+
});
|
|
131
|
+
await ensureDirectory(join(staged, "references"));
|
|
132
|
+
for (const file of shared)
|
|
133
|
+
await cp(file, join(staged, "references", basename(file)), {
|
|
134
|
+
errorOnExist: true,
|
|
135
|
+
force: false,
|
|
136
|
+
});
|
|
137
|
+
await validateSkillDirectory(staged);
|
|
138
|
+
return staged;
|
|
139
|
+
}
|
|
85
140
|
function safeTarget(root, target) {
|
|
86
141
|
const resolvedRoot = resolve(root);
|
|
87
142
|
const resolvedTarget = resolve(target);
|
|
@@ -95,6 +150,9 @@ export async function planSkillInstall(sourceRoot, targetDirectories, packageId,
|
|
|
95
150
|
const skills = await discoverSkillDirectories(sourceRoot, options);
|
|
96
151
|
if (skills.length === 0)
|
|
97
152
|
throw new Error(`No SKILL.md found under ${sourceRoot}`);
|
|
153
|
+
const sources = new Map();
|
|
154
|
+
for (const skill of skills)
|
|
155
|
+
sources.set(skill, await withSharedReferences(skill, sourceRoot));
|
|
98
156
|
const files = [];
|
|
99
157
|
for (const targetRoot of targetDirectories) {
|
|
100
158
|
try {
|
|
@@ -116,7 +174,7 @@ export async function planSkillInstall(sourceRoot, targetDirectories, packageId,
|
|
|
116
174
|
const target = safeTarget(targetRoot, join(targetRoot, name));
|
|
117
175
|
const frontmatter = await readFile(join(skill, "SKILL.md"), "utf8");
|
|
118
176
|
const skillName = frontmatter.match(/^name:\s*(\S+)/m)?.[1];
|
|
119
|
-
files.push({ source: skill, target, skillName });
|
|
177
|
+
files.push({ source: sources.get(skill) ?? skill, target, skillName });
|
|
120
178
|
}
|
|
121
179
|
}
|
|
122
180
|
const conflicts = detectInstallConflicts([
|
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
const LOCK_FILE = "coordination.lock";
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6
6
|
const MALFORMED_LOCK_STALE_MS = 30_000;
|
|
7
|
+
const localLockTails = new Map();
|
|
7
8
|
export class CoordinationLockTimeoutError extends Error {
|
|
8
9
|
constructor(timeoutMs) {
|
|
9
10
|
super(`Timed out waiting ${timeoutMs}ms for the coordination lock`);
|
|
@@ -64,9 +65,25 @@ async function releaseOwnedLock(path, token) {
|
|
|
64
65
|
function wait(delayMs) {
|
|
65
66
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
async function withLocalLock(path, operation) {
|
|
69
|
+
const previous = localLockTails.get(path) ?? Promise.resolve();
|
|
70
|
+
let release = () => undefined;
|
|
71
|
+
const current = new Promise((resolve) => {
|
|
72
|
+
release = resolve;
|
|
73
|
+
});
|
|
74
|
+
const tail = previous.then(() => current);
|
|
75
|
+
localLockTails.set(path, tail);
|
|
76
|
+
await previous;
|
|
77
|
+
try {
|
|
78
|
+
return await operation();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
release();
|
|
82
|
+
if (localLockTails.get(path) === tail)
|
|
83
|
+
localLockTails.delete(path);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function withFileLock(path, operation, timeoutMs) {
|
|
70
87
|
const deadline = Date.now() + timeoutMs;
|
|
71
88
|
const token = randomUUID();
|
|
72
89
|
const owner = {
|
|
@@ -138,3 +155,8 @@ export async function withCoordinationLock(coordinationDir, operation, timeoutMs
|
|
|
138
155
|
await releaseOwnedLock(path, token);
|
|
139
156
|
}
|
|
140
157
|
}
|
|
158
|
+
/** Serialize coordination mutations within this process and across processes. */
|
|
159
|
+
export async function withCoordinationLock(coordinationDir, operation, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
160
|
+
const path = join(coordinationDir, LOCK_FILE);
|
|
161
|
+
return withLocalLock(path, () => withFileLock(path, operation, timeoutMs));
|
|
162
|
+
}
|
|
@@ -5,8 +5,10 @@ Loadout — manage the skills, tools, and MCP servers your AI agents use.
|
|
|
5
5
|
FIRST TIME
|
|
6
6
|
|
|
7
7
|
loadout doctor which agents you have, and whether they are healthy
|
|
8
|
-
loadout setup --mode stable
|
|
9
|
-
|
|
8
|
+
loadout setup --mode stable --details
|
|
9
|
+
preview 30 reviewed skills and findings
|
|
10
|
+
loadout setup --mode stable --yes --approve-risk
|
|
11
|
+
install only after reviewing those findings
|
|
10
12
|
|
|
11
13
|
IN A PROJECT
|
|
12
14
|
|
|
@@ -34,7 +34,7 @@ const SPECIALIZED_WITHOUT_SIGNAL = /(?:appstore|msstore|backtesting|bats-|oracle
|
|
|
34
34
|
function hasProjectCompatibility(name, project) {
|
|
35
35
|
const hasLanguage = (language) => project.languages.includes(language);
|
|
36
36
|
const languageRequirements = [
|
|
37
|
-
[/(?:dotnet|csharp|aspnet|nuget|maui)/, ".net"],
|
|
37
|
+
[/(?:dotnet|csharp|aspnet|nuget|maui|blazor)/, ".net"],
|
|
38
38
|
[
|
|
39
39
|
/(?:python|pytest|fastapi|django|flask|pydantic|poetry|uv-package)/,
|
|
40
40
|
"python",
|