supered 0.1.3 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +19 -0
- package/bin/supered.mjs +8 -25
- 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/skill-design-principles.md +25 -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/manifest.js +7 -73
- package/lib/package-verification.js +113 -0
- package/lib/release-bundle.js +135 -0
- package/lib/supered-policy.js +32 -0
- package/package.json +1 -1
- package/skills/build-in-slices/SKILL.md +71 -15
- package/skills/make-a-map/SKILL.md +77 -15
- package/skills/prove-the-change/SKILL.md +73 -15
- package/skills/shape-the-task/SKILL.md +67 -14
- package/skills/ship-the-work/SKILL.md +75 -15
- package/skills/trace-the-fault/SKILL.md +71 -13
- package/skills/using-supered/SKILL.md +79 -11
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
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { HOST_TARGETS, SKILL_ORDER } from "./supered-policy.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const npmPackEnv = {
|
|
11
|
+
...process.env,
|
|
12
|
+
npm_config_dry_run: "false"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const PACKAGE_TARGETS = Object.keys(HOST_TARGETS);
|
|
16
|
+
export const REQUIRED_PACKAGE_FILES = [
|
|
17
|
+
".claude-plugin/",
|
|
18
|
+
".codex-plugin/",
|
|
19
|
+
".cursor-plugin/",
|
|
20
|
+
".opencode/",
|
|
21
|
+
"1.svg",
|
|
22
|
+
"assets/",
|
|
23
|
+
"bin/",
|
|
24
|
+
"docs/",
|
|
25
|
+
"gemini-extension.json",
|
|
26
|
+
"install.sh",
|
|
27
|
+
"lib/",
|
|
28
|
+
"skills/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
];
|
|
32
|
+
export const EXCLUDED_PACKAGE_PREFIXES = [
|
|
33
|
+
"tests/",
|
|
34
|
+
"artifacts/",
|
|
35
|
+
"node_modules/"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
async function readJson(path) {
|
|
39
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function packageFilePaths(pack) {
|
|
43
|
+
return pack.files.map((file) => file.path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function inspectPackageFiles(files) {
|
|
47
|
+
return {
|
|
48
|
+
missingRequiredFiles: REQUIRED_PACKAGE_FILES.filter((path) => {
|
|
49
|
+
if (path.endsWith("/")) {
|
|
50
|
+
return !files.some((file) => file.startsWith(path));
|
|
51
|
+
}
|
|
52
|
+
return !files.includes(path);
|
|
53
|
+
}),
|
|
54
|
+
excludedFileViolations: files.filter((file) => EXCLUDED_PACKAGE_PREFIXES.some((prefix) => file.startsWith(prefix)))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function verifyNpmPackage({ root = process.cwd(), tempRoot } = {}) {
|
|
59
|
+
const workRoot = tempRoot ?? await mkdtemp(join(tmpdir(), "supered-package-"));
|
|
60
|
+
const cleanup = !tempRoot;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const packageJson = await readJson(join(root, "package.json"));
|
|
64
|
+
const { stdout } = await execFileAsync("npm", ["pack", "--pack-destination", workRoot, "--json"], {
|
|
65
|
+
cwd: root,
|
|
66
|
+
env: npmPackEnv
|
|
67
|
+
});
|
|
68
|
+
const [pack] = JSON.parse(stdout);
|
|
69
|
+
const tarball = join(workRoot, pack.filename);
|
|
70
|
+
await stat(tarball);
|
|
71
|
+
|
|
72
|
+
const files = packageFilePaths(pack);
|
|
73
|
+
const fileInspection = inspectPackageFiles(files);
|
|
74
|
+
|
|
75
|
+
if (fileInspection.missingRequiredFiles.length > 0) {
|
|
76
|
+
throw new Error(`Npm package is missing required files: ${fileInspection.missingRequiredFiles.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
if (fileInspection.excludedFileViolations.length > 0) {
|
|
79
|
+
throw new Error(`Npm package includes local-only files: ${fileInspection.excludedFileViolations.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const target of PACKAGE_TARGETS) {
|
|
83
|
+
const dest = join(workRoot, target);
|
|
84
|
+
await execFileAsync(
|
|
85
|
+
"npm",
|
|
86
|
+
["exec", "--yes", "--package", tarball, "--", "supered", "install", "--target", target, "--dest", dest],
|
|
87
|
+
{ cwd: root, env: npmPackEnv }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
for (const skill of SKILL_ORDER) {
|
|
91
|
+
await stat(join(dest, skill, "SKILL.md"));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = {
|
|
96
|
+
packageName: packageJson.name,
|
|
97
|
+
version: packageJson.version,
|
|
98
|
+
tarballName: pack.filename,
|
|
99
|
+
files,
|
|
100
|
+
installedTargets: [...PACKAGE_TARGETS],
|
|
101
|
+
installedSkills: [...SKILL_ORDER],
|
|
102
|
+
...fileInspection
|
|
103
|
+
};
|
|
104
|
+
if (!cleanup) {
|
|
105
|
+
result.tarball = tarball;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
} finally {
|
|
109
|
+
if (cleanup) {
|
|
110
|
+
await rm(workRoot, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { collectManifest, listSkills } from "./manifest.js";
|
|
5
|
+
import { REQUIRED_PACKAGE_FILES } from "./package-verification.js";
|
|
6
|
+
import { HOST_TARGETS, SKILL_ORDER } from "./supered-policy.js";
|
|
7
|
+
|
|
8
|
+
async function readText(root, path) {
|
|
9
|
+
return readFile(join(root, path), "utf8");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function add(errorList, condition, message) {
|
|
13
|
+
if (!condition) {
|
|
14
|
+
errorList.push(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pushChecked(checked, paths) {
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
if (!checked.includes(path)) {
|
|
21
|
+
checked.push(path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validateReleaseBundle(root = process.cwd()) {
|
|
27
|
+
const errors = [];
|
|
28
|
+
const checked = [];
|
|
29
|
+
const manifest = await collectManifest(root);
|
|
30
|
+
const version = manifest.package.version;
|
|
31
|
+
const hostTargets = Object.keys(HOST_TARGETS);
|
|
32
|
+
|
|
33
|
+
pushChecked(checked, [
|
|
34
|
+
"package.json",
|
|
35
|
+
".codex-plugin/plugin.json",
|
|
36
|
+
".claude-plugin/plugin.json",
|
|
37
|
+
".cursor-plugin/plugin.json",
|
|
38
|
+
"gemini-extension.json"
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
add(errors, manifest.package.name === "supered", "package.json name must be supered");
|
|
42
|
+
add(errors, manifest.package.publishConfig?.access === "public", "package.json publishConfig.access must be public");
|
|
43
|
+
add(errors, manifest.package.bin?.supered === "bin/supered.mjs", "package.json bin.supered must point at bin/supered.mjs");
|
|
44
|
+
add(errors, manifest.package.scripts?.["verify-site"] === "node ./scripts/verify-site.mjs", "verify-site script is missing");
|
|
45
|
+
add(
|
|
46
|
+
errors,
|
|
47
|
+
manifest.package.scripts?.["verify-package"] === "node ./scripts/verify-npm-package.mjs",
|
|
48
|
+
"verify-package script is missing"
|
|
49
|
+
);
|
|
50
|
+
add(
|
|
51
|
+
errors,
|
|
52
|
+
JSON.stringify(manifest.package.files) === JSON.stringify(REQUIRED_PACKAGE_FILES),
|
|
53
|
+
"package.json files list is not aligned"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
add(errors, manifest.codexPlugin.name === "supered", ".codex-plugin/plugin.json name must be supered");
|
|
57
|
+
add(errors, manifest.codexPlugin.version === version, "Codex plugin version must match package.json");
|
|
58
|
+
add(errors, manifest.claudePlugin.version === version, "Claude plugin version must match package.json");
|
|
59
|
+
add(errors, manifest.cursorPlugin.version === version, "Cursor plugin version must match package.json");
|
|
60
|
+
add(errors, manifest.geminiExtension.version === version, "Gemini extension version must match package.json");
|
|
61
|
+
add(errors, manifest.codexPlugin.interface?.displayName === "Supered", "Codex plugin displayName must be Supered");
|
|
62
|
+
add(errors, manifest.codexPlugin.skills === "./skills/", "Codex plugin skills path must be ./skills/");
|
|
63
|
+
add(
|
|
64
|
+
errors,
|
|
65
|
+
JSON.stringify(manifest.codexPlugin.interface?.screenshots ?? []) === JSON.stringify(["./docs/preview.svg"]),
|
|
66
|
+
"Codex plugin screenshots must include docs preview"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const logoPath = manifest.codexPlugin.interface?.logo?.replace(/^\.\//, "");
|
|
70
|
+
if (logoPath) {
|
|
71
|
+
pushChecked(checked, [logoPath]);
|
|
72
|
+
try {
|
|
73
|
+
await access(join(root, logoPath));
|
|
74
|
+
} catch {
|
|
75
|
+
errors.push(`Missing logo file: ${logoPath}`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
errors.push("Codex plugin interface.logo is required");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const skills = await listSkills(root);
|
|
82
|
+
for (const skill of skills) {
|
|
83
|
+
checked.push(relative(root, skill.path));
|
|
84
|
+
add(errors, Boolean(skill.name), `${relative(root, skill.path)} is missing a name`);
|
|
85
|
+
add(errors, Boolean(skill.description), `${relative(root, skill.path)} is missing a description`);
|
|
86
|
+
add(errors, /^# /m.test(skill.body), `${relative(root, skill.path)} is missing an H1`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const skillNames = skills.map((skill) => skill.name);
|
|
90
|
+
for (const expected of SKILL_ORDER) {
|
|
91
|
+
add(errors, skillNames.includes(expected), `Missing skill: ${expected}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pushChecked(checked, [
|
|
95
|
+
".github/workflows/ci.yml",
|
|
96
|
+
"RELEASE_NOTES.md",
|
|
97
|
+
"docs/roadmap.md",
|
|
98
|
+
"docs/marketplace-checklist.md",
|
|
99
|
+
"docs/index.html",
|
|
100
|
+
"README.md",
|
|
101
|
+
"install.sh"
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const ci = await readText(root, ".github/workflows/ci.yml");
|
|
105
|
+
for (const command of ["npm ci", "npm test", "npm run validate", "npm run smoke-install", "npm run verify-package", "npm run verify-site"]) {
|
|
106
|
+
add(errors, ci.includes(command), `CI must run ${command}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const releaseNotes = await readText(root, "RELEASE_NOTES.md");
|
|
110
|
+
add(errors, releaseNotes.includes(`# Supered v${version}`), `Release notes must include v${version}`);
|
|
111
|
+
|
|
112
|
+
const roadmap = await readText(root, "docs/roadmap.md");
|
|
113
|
+
add(errors, /^# Supered Roadmap/m.test(roadmap), "Roadmap must have a Supered heading");
|
|
114
|
+
|
|
115
|
+
const marketplace = await readText(root, "docs/marketplace-checklist.md");
|
|
116
|
+
add(errors, marketplace.includes("install.sh"), "Marketplace checklist must mention install.sh");
|
|
117
|
+
add(errors, marketplace.includes("verify-package"), "Marketplace checklist must mention package verification");
|
|
118
|
+
|
|
119
|
+
for (const host of hostTargets) {
|
|
120
|
+
const hostDoc = `docs/hosts/${host}.md`;
|
|
121
|
+
pushChecked(checked, [hostDoc]);
|
|
122
|
+
const doc = await readText(root, hostDoc);
|
|
123
|
+
add(errors, new RegExp(`# .*${host}`, "i").test(doc), `${hostDoc} must name the host`);
|
|
124
|
+
add(errors, doc.includes("install.sh"), `${hostDoc} must mention install.sh`);
|
|
125
|
+
add(errors, /SUPERED_TARGET|--target/.test(doc), `${hostDoc} must document target selection`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
errors,
|
|
130
|
+
checked,
|
|
131
|
+
skills,
|
|
132
|
+
version,
|
|
133
|
+
hostTargets
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SKILL_ORDER = [
|
|
4
|
+
"using-supered",
|
|
5
|
+
"shape-the-task",
|
|
6
|
+
"make-a-map",
|
|
7
|
+
"build-in-slices",
|
|
8
|
+
"trace-the-fault",
|
|
9
|
+
"prove-the-change",
|
|
10
|
+
"ship-the-work"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const HOST_TARGETS = {
|
|
14
|
+
codex: ".codex/skills",
|
|
15
|
+
claude: ".claude/skills",
|
|
16
|
+
cursor: ".cursor/skills",
|
|
17
|
+
gemini: ".gemini/skills",
|
|
18
|
+
opencode: ".opencode/skills"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function defaultInstallDest(target, home = process.env.HOME) {
|
|
22
|
+
if (!home) {
|
|
23
|
+
throw new Error("HOME is not set; pass --dest explicitly.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const targetPath = HOST_TARGETS[target];
|
|
27
|
+
if (!targetPath) {
|
|
28
|
+
throw new Error(`Unsupported target: ${target}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return join(home, targetPath);
|
|
32
|
+
}
|
package/package.json
CHANGED