supered 0.2.0 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +28 -1
- package/bin/supered.mjs +45 -23
- package/docs/evals/README.md +34 -0
- package/docs/evals/baseline-results.json +132 -0
- package/docs/evals/scenarios.json +212 -0
- package/docs/index.html +23 -0
- package/docs/install.md +16 -0
- package/docs/roadmap.md +1 -0
- package/docs/styles.css +40 -0
- package/docs/which-skill.md +33 -0
- package/gemini-extension.json +1 -1
- package/install.sh +37 -1
- package/lib/eval-pack.js +101 -0
- package/lib/host-install.js +88 -0
- package/lib/install-doctor.js +181 -0
- package/lib/manifest.js +7 -73
- package/lib/package-verification.js +120 -0
- package/lib/release-bundle.js +135 -0
- package/lib/supered-policy.js +32 -0
- package/package.json +1 -1
package/docs/roadmap.md
CHANGED
package/docs/styles.css
CHANGED
|
@@ -176,6 +176,7 @@ h3 {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
.workflow,
|
|
179
|
+
.proof,
|
|
179
180
|
.install {
|
|
180
181
|
border-top: 1px solid var(--line);
|
|
181
182
|
padding: 68px clamp(24px, 6vw, 84px);
|
|
@@ -209,12 +210,47 @@ h3 {
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
.steps p,
|
|
213
|
+
.proof p,
|
|
212
214
|
.install p {
|
|
213
215
|
color: var(--muted);
|
|
214
216
|
font-size: 18px;
|
|
215
217
|
line-height: 1.45;
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
.proof-grid {
|
|
221
|
+
display: grid;
|
|
222
|
+
gap: 16px;
|
|
223
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
224
|
+
margin-top: 30px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.proof-grid a {
|
|
228
|
+
background: white;
|
|
229
|
+
border: 1px solid var(--line);
|
|
230
|
+
border-radius: 8px;
|
|
231
|
+
display: grid;
|
|
232
|
+
gap: 14px;
|
|
233
|
+
min-height: 150px;
|
|
234
|
+
padding: 22px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.proof-grid a:hover {
|
|
238
|
+
border-color: var(--teal);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.proof-grid span {
|
|
242
|
+
color: var(--orange);
|
|
243
|
+
font-size: 13px;
|
|
244
|
+
font-weight: 850;
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.proof-grid strong {
|
|
249
|
+
color: var(--navy);
|
|
250
|
+
font-size: 22px;
|
|
251
|
+
line-height: 1.15;
|
|
252
|
+
}
|
|
253
|
+
|
|
218
254
|
.install {
|
|
219
255
|
align-items: start;
|
|
220
256
|
display: grid;
|
|
@@ -269,6 +305,10 @@ code {
|
|
|
269
305
|
grid-template-columns: 1fr;
|
|
270
306
|
}
|
|
271
307
|
|
|
308
|
+
.proof-grid {
|
|
309
|
+
grid-template-columns: 1fr;
|
|
310
|
+
}
|
|
311
|
+
|
|
272
312
|
.hero-media {
|
|
273
313
|
justify-items: start;
|
|
274
314
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Which Skill Should I Use?
|
|
2
|
+
|
|
3
|
+
Start with the shape of the work, not the name of the tool.
|
|
4
|
+
|
|
5
|
+
| Situation | Use this skill | Why |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| The request is broad, mixed, or risky and you are not sure where to begin. | `using-supered` | Routes the session to the right next workflow. |
|
|
8
|
+
| The user asks for something vague, underspecified, or easy to overbuild. | `shape-the-task` | Produces a short brief, assumptions, non-goals, and acceptance signals. |
|
|
9
|
+
| The direction is approved but the files, order, or checks are unclear. | `make-a-map` | Turns intent into an execution map with checkpoints and verification. |
|
|
10
|
+
| You are implementing code, docs, packaging, or site changes. | `build-in-slices` | Keeps work small, reviewable, and easier to recover from. |
|
|
11
|
+
| Something is broken, flaky, confusing, or explained only by guesses. | `trace-the-fault` | Forces symptom capture, hypotheses, probes, and evidence before patches. |
|
|
12
|
+
| You are about to say work is done, fixed, published, or ready. | `prove-the-change` | Requires fresh proof before any completion claim. |
|
|
13
|
+
| The work needs commit, push, release, deploy, publish, or public handoff. | `ship-the-work` | Makes shipping part of the work and preserves evidence for readers. |
|
|
14
|
+
|
|
15
|
+
## Fast Routing
|
|
16
|
+
|
|
17
|
+
- If the task is unclear: start with `shape-the-task`.
|
|
18
|
+
- If the task is clear but large: start with `make-a-map`.
|
|
19
|
+
- If the task is already planned: start with `build-in-slices`.
|
|
20
|
+
- If behavior is broken: start with `trace-the-fault`.
|
|
21
|
+
- If you are making a claim: start with `prove-the-change`.
|
|
22
|
+
- If the work leaves your machine: start with `ship-the-work`.
|
|
23
|
+
|
|
24
|
+
## Combining Skills
|
|
25
|
+
|
|
26
|
+
Good sessions often use two or three skills in order:
|
|
27
|
+
|
|
28
|
+
- New feature: `shape-the-task` -> `make-a-map` -> `build-in-slices` -> `prove-the-change`.
|
|
29
|
+
- Bug fix: `trace-the-fault` -> `build-in-slices` -> `prove-the-change`.
|
|
30
|
+
- Public release: `prove-the-change` -> `ship-the-work`.
|
|
31
|
+
- Messy request: `using-supered` -> whichever skill the routing decision selects.
|
|
32
|
+
|
|
33
|
+
Do not load every skill just because they exist. Use the smallest workflow that changes the outcome.
|
package/gemini-extension.json
CHANGED
package/install.sh
CHANGED
|
@@ -6,6 +6,13 @@ SUPERED_REF="${SUPERED_REF:-main}"
|
|
|
6
6
|
SUPERED_TARGET="${SUPERED_TARGET:-codex}"
|
|
7
7
|
SUPERED_SOURCE_DIR="${SUPERED_SOURCE_DIR:-}"
|
|
8
8
|
SUPERED_DEST="${SUPERED_DEST:-}"
|
|
9
|
+
SUPERED_SKILLS='using-supered
|
|
10
|
+
shape-the-task
|
|
11
|
+
make-a-map
|
|
12
|
+
build-in-slices
|
|
13
|
+
trace-the-fault
|
|
14
|
+
prove-the-change
|
|
15
|
+
ship-the-work'
|
|
9
16
|
|
|
10
17
|
usage() {
|
|
11
18
|
cat <<'EOF'
|
|
@@ -71,6 +78,23 @@ default_dest() {
|
|
|
71
78
|
esac
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
reject_symlink() {
|
|
82
|
+
[ ! -L "$1" ] || die "Refusing to install symlink: $1"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
reject_source_symlinks() {
|
|
86
|
+
reject_symlink "$1"
|
|
87
|
+
linked="$(find "$1" -type l -print -quit)"
|
|
88
|
+
[ -z "$linked" ] || die "Refusing to install symlink: $linked"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
reject_destination_symlinks() {
|
|
92
|
+
[ ! -L "$dest" ] || die "Refusing to install into symlinked destination: $dest"
|
|
93
|
+
for skill in $SUPERED_SKILLS; do
|
|
94
|
+
[ ! -L "$dest/$skill" ] || die "Refusing to install into symlinked destination: $dest/$skill"
|
|
95
|
+
done
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
download_source() {
|
|
75
99
|
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/supered.XXXXXX")"
|
|
76
100
|
archive="$tmp_dir/supered.tar.gz"
|
|
@@ -100,7 +124,19 @@ if [ -z "$dest" ]; then
|
|
|
100
124
|
dest="$(default_dest)"
|
|
101
125
|
fi
|
|
102
126
|
|
|
127
|
+
for skill in $SUPERED_SKILLS; do
|
|
128
|
+
skill_dir="$source_dir/skills/$skill"
|
|
129
|
+
[ -d "$skill_dir" ] || die "Missing skill directory: $skill_dir"
|
|
130
|
+
reject_source_symlinks "$skill_dir"
|
|
131
|
+
[ -f "$skill_dir/SKILL.md" ] || die "Missing skill file: $skill_dir/SKILL.md"
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
reject_destination_symlinks
|
|
103
135
|
mkdir -p "$dest"
|
|
104
|
-
|
|
136
|
+
reject_destination_symlinks
|
|
137
|
+
|
|
138
|
+
for skill in $SUPERED_SKILLS; do
|
|
139
|
+
cp -R "$source_dir/skills/$skill" "$dest/"
|
|
140
|
+
done
|
|
105
141
|
|
|
106
142
|
printf 'Installed Supered for %s at %s\n' "$SUPERED_TARGET" "$dest"
|
package/lib/eval-pack.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { SKILL_ORDER } from "./supered-policy.js";
|
|
5
|
+
|
|
6
|
+
export const EVAL_CATALOG_VERSION = "0.1";
|
|
7
|
+
export const SCORING_DIMENSIONS = [
|
|
8
|
+
"clarity",
|
|
9
|
+
"actionability",
|
|
10
|
+
"guardrails",
|
|
11
|
+
"evidence",
|
|
12
|
+
"outcome"
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function readJson(path) {
|
|
16
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function add(errorList, condition, message) {
|
|
20
|
+
if (!condition) {
|
|
21
|
+
errorList.push(message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readEvalPack(root = process.cwd()) {
|
|
26
|
+
return {
|
|
27
|
+
catalog: await readJson(join(root, "docs", "evals", "scenarios.json")),
|
|
28
|
+
report: await readJson(join(root, "docs", "evals", "baseline-results.json"))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function validateEvalPack(root = process.cwd()) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
const { catalog, report } = await readEvalPack(root);
|
|
35
|
+
const scenarioIds = new Set();
|
|
36
|
+
const coveredSkills = new Set();
|
|
37
|
+
|
|
38
|
+
add(errors, catalog.product === "Supered", "Eval Pack catalog product must be Supered");
|
|
39
|
+
add(errors, catalog.version === EVAL_CATALOG_VERSION, `Eval Pack catalog version must be ${EVAL_CATALOG_VERSION}`);
|
|
40
|
+
add(errors, catalog.scoring?.maxScore === 5, "Eval Pack scoring maxScore must be 5");
|
|
41
|
+
add(
|
|
42
|
+
errors,
|
|
43
|
+
JSON.stringify(catalog.scoring?.dimensions ?? []) === JSON.stringify(SCORING_DIMENSIONS),
|
|
44
|
+
"Eval Pack scoring dimensions are not aligned"
|
|
45
|
+
);
|
|
46
|
+
add(errors, catalog.scenarios?.length === 10, "Eval Pack must include 10 scenarios");
|
|
47
|
+
|
|
48
|
+
for (const scenario of catalog.scenarios ?? []) {
|
|
49
|
+
add(errors, /^S\d{2}$/.test(scenario.id), `${scenario.id} must use an S00-style id`);
|
|
50
|
+
add(errors, !scenarioIds.has(scenario.id), `${scenario.id} must be unique`);
|
|
51
|
+
scenarioIds.add(scenario.id);
|
|
52
|
+
|
|
53
|
+
add(errors, scenario.title?.length >= 12, `${scenario.id} needs a specific title`);
|
|
54
|
+
add(errors, scenario.prompt?.length >= 80, `${scenario.id} prompt is too thin`);
|
|
55
|
+
add(errors, scenario.context?.length >= 80, `${scenario.id} context is too thin`);
|
|
56
|
+
add(errors, scenario.successCriteria?.length >= 3, `${scenario.id} needs success criteria`);
|
|
57
|
+
add(errors, scenario.expectedEvidence?.length >= 2, `${scenario.id} needs evidence expectations`);
|
|
58
|
+
add(errors, scenario.primarySkills?.length >= 1, `${scenario.id} needs a primary skill`);
|
|
59
|
+
|
|
60
|
+
for (const skill of scenario.primarySkills ?? []) {
|
|
61
|
+
coveredSkills.add(skill);
|
|
62
|
+
add(errors, SKILL_ORDER.includes(skill), `${scenario.id} references unknown skill ${skill}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const skill of SKILL_ORDER) {
|
|
67
|
+
add(errors, coveredSkills.has(skill), `Eval Pack does not exercise ${skill}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
add(errors, report.product === "Supered", "Eval Pack report product must be Supered");
|
|
71
|
+
add(errors, report.catalogVersion === EVAL_CATALOG_VERSION, "Eval Pack report catalogVersion is not aligned");
|
|
72
|
+
add(errors, report.results?.length === 10, "Eval Pack report must include 10 results");
|
|
73
|
+
add(errors, report.summary?.averageScore >= 4.2, "Eval Pack baseline average should show useful outcomes");
|
|
74
|
+
|
|
75
|
+
for (const result of report.results ?? []) {
|
|
76
|
+
add(errors, scenarioIds.has(result.scenarioId), `${result.scenarioId} does not match a known scenario`);
|
|
77
|
+
add(
|
|
78
|
+
errors,
|
|
79
|
+
JSON.stringify(Object.keys(result.scores ?? {}).sort()) === JSON.stringify([...SCORING_DIMENSIONS].sort()),
|
|
80
|
+
`${result.scenarioId} scores must cover every scoring dimension`
|
|
81
|
+
);
|
|
82
|
+
for (const score of Object.values(result.scores ?? {})) {
|
|
83
|
+
add(errors, Number.isInteger(score), "Eval Pack scores must be integers");
|
|
84
|
+
add(errors, score >= 1 && score <= 5, "Eval Pack scores must be between 1 and 5");
|
|
85
|
+
}
|
|
86
|
+
add(errors, result.notes?.length >= 80, `${result.scenarioId} needs explanatory scoring notes`);
|
|
87
|
+
add(errors, result.recommendedSkill?.length > 0, `${result.scenarioId} needs a recommended skill`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
errors,
|
|
92
|
+
catalog,
|
|
93
|
+
report,
|
|
94
|
+
summary: {
|
|
95
|
+
scenarioCount: catalog.scenarios?.length ?? 0,
|
|
96
|
+
resultCount: report.results?.length ?? 0,
|
|
97
|
+
averageScore: report.summary?.averageScore ?? 0,
|
|
98
|
+
coveredSkills: SKILL_ORDER.filter((skill) => coveredSkills.has(skill))
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cp, lstat, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { defaultInstallDest, SKILL_ORDER } from "./supered-policy.js";
|
|
5
|
+
|
|
6
|
+
async function assertNoSymlinks(path) {
|
|
7
|
+
const entry = await lstat(path);
|
|
8
|
+
if (entry.isSymbolicLink()) {
|
|
9
|
+
throw new Error(`Refusing to install symlink: ${path}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!entry.isDirectory()) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const children = await readdir(path);
|
|
17
|
+
await Promise.all(children.map((child) => assertNoSymlinks(join(path, child))));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function assertSkillIsInstallable(skillsDir, skill) {
|
|
21
|
+
const skillDir = join(skillsDir, skill);
|
|
22
|
+
await assertNoSymlinks(skillDir);
|
|
23
|
+
|
|
24
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
25
|
+
try {
|
|
26
|
+
await stat(skillFile);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === "ENOENT") {
|
|
29
|
+
throw new Error(`Missing skill file: ${skillFile}`);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function assertDestinationPathIsSafe(dest) {
|
|
36
|
+
try {
|
|
37
|
+
const destination = await lstat(dest);
|
|
38
|
+
if (destination.isSymbolicLink()) {
|
|
39
|
+
throw new Error(`Refusing to install into symlinked destination: ${dest}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT") {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function assertManagedDestinationsAreSafe(dest) {
|
|
49
|
+
for (const skill of SKILL_ORDER) {
|
|
50
|
+
const skillDest = join(dest, skill);
|
|
51
|
+
try {
|
|
52
|
+
const destination = await lstat(skillDest);
|
|
53
|
+
if (destination.isSymbolicLink()) {
|
|
54
|
+
throw new Error(`Refusing to install into symlinked destination: ${skillDest}`);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code !== "ENOENT") {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function installSuperedSkills({ root = process.cwd(), target, dest, home = process.env.HOME } = {}) {
|
|
65
|
+
if (!target) {
|
|
66
|
+
throw new Error("Install requires --target.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const installDest = dest ?? defaultInstallDest(target, home);
|
|
70
|
+
const skillsDir = resolve(root, "skills");
|
|
71
|
+
|
|
72
|
+
await assertDestinationPathIsSafe(installDest);
|
|
73
|
+
await assertManagedDestinationsAreSafe(installDest);
|
|
74
|
+
for (const skill of SKILL_ORDER) {
|
|
75
|
+
await assertSkillIsInstallable(skillsDir, skill);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await mkdir(installDest, { recursive: true });
|
|
79
|
+
for (const skill of SKILL_ORDER) {
|
|
80
|
+
await cp(join(skillsDir, skill), join(installDest, skill), { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
target,
|
|
85
|
+
dest: installDest,
|
|
86
|
+
installedSkills: [...SKILL_ORDER]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { defaultInstallDest, SKILL_ORDER } from "./supered-policy.js";
|
|
5
|
+
|
|
6
|
+
function shellQuote(value) {
|
|
7
|
+
if (!/[\s'"\\]/.test(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fixCommand(target, dest) {
|
|
14
|
+
return `npx supered@latest install --target ${target} --dest ${shellQuote(dest)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function issue(code, message, extra = {}) {
|
|
18
|
+
return {
|
|
19
|
+
code,
|
|
20
|
+
message,
|
|
21
|
+
...extra
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function lstatOrNull(path) {
|
|
26
|
+
try {
|
|
27
|
+
return await lstat(path);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function findSymlink(path) {
|
|
37
|
+
const entry = await lstatOrNull(path);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (entry.isSymbolicLink()) {
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
if (!entry.isDirectory()) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const children = await readdir(path);
|
|
49
|
+
for (const child of children) {
|
|
50
|
+
const linked = await findSymlink(join(path, child));
|
|
51
|
+
if (linked) {
|
|
52
|
+
return linked;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readInstalledSkill(dest, skill) {
|
|
59
|
+
const skillDir = join(dest, skill);
|
|
60
|
+
const linked = await findSymlink(skillDir);
|
|
61
|
+
if (linked) {
|
|
62
|
+
return {
|
|
63
|
+
linked
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
68
|
+
const fileStat = await lstatOrNull(skillFile);
|
|
69
|
+
if (!fileStat) {
|
|
70
|
+
return {
|
|
71
|
+
missing: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (fileStat.isSymbolicLink()) {
|
|
75
|
+
return {
|
|
76
|
+
linked: skillFile
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
contents: await readFile(skillFile, "utf8")
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function inspectSuperedInstall({ root = process.cwd(), target, dest, home = process.env.HOME } = {}) {
|
|
86
|
+
if (!target) {
|
|
87
|
+
throw new Error("Doctor requires --target.");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const installDest = dest ?? defaultInstallDest(target, home);
|
|
91
|
+
const sourceSkillsDir = resolve(root, "skills");
|
|
92
|
+
const issues = [];
|
|
93
|
+
const missingSkills = [];
|
|
94
|
+
const changedSkills = [];
|
|
95
|
+
const installedSkills = [];
|
|
96
|
+
const command = fixCommand(target, installDest);
|
|
97
|
+
const destStat = await lstatOrNull(installDest);
|
|
98
|
+
|
|
99
|
+
if (!destStat) {
|
|
100
|
+
return {
|
|
101
|
+
target,
|
|
102
|
+
dest: installDest,
|
|
103
|
+
status: "issues",
|
|
104
|
+
issues: [
|
|
105
|
+
issue("missing-destination", `Install destination does not exist: ${installDest}`, {
|
|
106
|
+
path: installDest
|
|
107
|
+
})
|
|
108
|
+
],
|
|
109
|
+
installedSkills,
|
|
110
|
+
missingSkills: [...SKILL_ORDER],
|
|
111
|
+
changedSkills,
|
|
112
|
+
fixCommand: command
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (destStat.isSymbolicLink()) {
|
|
117
|
+
return {
|
|
118
|
+
target,
|
|
119
|
+
dest: installDest,
|
|
120
|
+
status: "issues",
|
|
121
|
+
issues: [
|
|
122
|
+
issue("symlinked-destination", `Install destination is a symlink: ${installDest}`, {
|
|
123
|
+
path: installDest
|
|
124
|
+
})
|
|
125
|
+
],
|
|
126
|
+
installedSkills,
|
|
127
|
+
missingSkills,
|
|
128
|
+
changedSkills,
|
|
129
|
+
fixCommand: command
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const skill of SKILL_ORDER) {
|
|
134
|
+
const expectedPath = join(sourceSkillsDir, skill, "SKILL.md");
|
|
135
|
+
const expected = await readFile(expectedPath, "utf8");
|
|
136
|
+
const installed = await readInstalledSkill(installDest, skill);
|
|
137
|
+
|
|
138
|
+
if (installed.linked) {
|
|
139
|
+
issues.push(
|
|
140
|
+
issue("unsafe-symlink", `Managed skill contains a symlink: ${installed.linked}`, {
|
|
141
|
+
skill,
|
|
142
|
+
path: installed.linked
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (installed.missing) {
|
|
149
|
+
missingSkills.push(skill);
|
|
150
|
+
issues.push(
|
|
151
|
+
issue("missing-skill", `Missing installed skill file: ${join(installDest, skill, "SKILL.md")}`, {
|
|
152
|
+
skill,
|
|
153
|
+
path: join(installDest, skill, "SKILL.md")
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
installedSkills.push(skill);
|
|
160
|
+
if (installed.contents !== expected) {
|
|
161
|
+
changedSkills.push(skill);
|
|
162
|
+
issues.push(
|
|
163
|
+
issue("changed-skill", `Installed skill differs from this Supered bundle: ${skill}`, {
|
|
164
|
+
skill,
|
|
165
|
+
path: join(installDest, skill, "SKILL.md")
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
target,
|
|
173
|
+
dest: installDest,
|
|
174
|
+
status: issues.length === 0 ? "ok" : "issues",
|
|
175
|
+
issues,
|
|
176
|
+
installedSkills,
|
|
177
|
+
missingSkills,
|
|
178
|
+
changedSkills,
|
|
179
|
+
fixCommand: command
|
|
180
|
+
};
|
|
181
|
+
}
|
package/lib/manifest.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"make-a-map",
|
|
8
|
-
"build-in-slices",
|
|
9
|
-
"trace-the-fault",
|
|
10
|
-
"prove-the-change",
|
|
11
|
-
"ship-the-work"
|
|
12
|
-
];
|
|
4
|
+
import { SKILL_ORDER } from "./supered-policy.js";
|
|
5
|
+
|
|
6
|
+
export { SKILL_ORDER } from "./supered-policy.js";
|
|
13
7
|
|
|
14
8
|
async function readJson(path) {
|
|
15
9
|
return JSON.parse(await readFile(path, "utf8"));
|
|
@@ -72,66 +66,6 @@ export async function collectManifest(root = process.cwd()) {
|
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
export async function validateProject(root = process.cwd()) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const manifest = await collectManifest(root);
|
|
78
|
-
checked.push(".codex-plugin/plugin.json");
|
|
79
|
-
checked.push(".claude-plugin/plugin.json");
|
|
80
|
-
checked.push(".cursor-plugin/plugin.json");
|
|
81
|
-
checked.push("gemini-extension.json");
|
|
82
|
-
|
|
83
|
-
if (manifest.package.name !== "supered") {
|
|
84
|
-
errors.push("package.json name must be supered");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (manifest.codexPlugin.name !== "supered") {
|
|
88
|
-
errors.push(".codex-plugin/plugin.json name must be supered");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (manifest.codexPlugin.interface?.displayName !== "Supered") {
|
|
92
|
-
errors.push("Codex plugin displayName must be Supered");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (manifest.codexPlugin.skills !== "./skills/") {
|
|
96
|
-
errors.push("Codex plugin skills path must be ./skills/");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const logoPath = manifest.codexPlugin.interface?.logo?.replace(/^\.\//, "");
|
|
100
|
-
if (logoPath) {
|
|
101
|
-
checked.push(logoPath);
|
|
102
|
-
try {
|
|
103
|
-
await access(join(root, logoPath));
|
|
104
|
-
} catch {
|
|
105
|
-
errors.push(`Missing logo file: ${logoPath}`);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
errors.push("Codex plugin interface.logo is required");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const skills = await listSkills(root);
|
|
112
|
-
for (const skill of skills) {
|
|
113
|
-
checked.push(relative(root, skill.path));
|
|
114
|
-
if (!skill.name) {
|
|
115
|
-
errors.push(`${relative(root, skill.path)} is missing a name`);
|
|
116
|
-
}
|
|
117
|
-
if (!skill.description) {
|
|
118
|
-
errors.push(`${relative(root, skill.path)} is missing a description`);
|
|
119
|
-
}
|
|
120
|
-
if (!/^# /m.test(skill.body)) {
|
|
121
|
-
errors.push(`${relative(root, skill.path)} is missing an H1`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const skillNames = skills.map((skill) => skill.name);
|
|
126
|
-
for (const expected of SKILL_ORDER) {
|
|
127
|
-
if (!skillNames.includes(expected)) {
|
|
128
|
-
errors.push(`Missing skill: ${expected}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
errors,
|
|
134
|
-
checked,
|
|
135
|
-
skills
|
|
136
|
-
};
|
|
69
|
+
const { validateReleaseBundle } = await import("./release-bundle.js");
|
|
70
|
+
return validateReleaseBundle(root);
|
|
137
71
|
}
|