Files
ECC/scripts/build-pi-core.js
T
Affaan MustafaandGitHub c70874fae9 feat(pi): curated pi/core skills+prompts profile, CI load test, and 2.2.2 release sync (#3264)
Adds a curated, Pi-native, skills+prompts-only profile at pi/core/ for downstream packagers that mirror GitHub Releases.

- manifests/pi-core.json: explicit include lists, per-item exclusion reasons, curation rules, safety allowlists; every root skill and command must be classified.
- scripts/build-pi-core.js regenerates pi/core deterministically (package.json from VERSION, LICENSE, README.md, CURATION.md, skills/, commands/); --check fails CI on drift. council is renamed ecc-council inside pi/core only.
- Safety checks: no callable endpoints outside the allowlist, no npx/curl|sh/pip install, no secrets, no absolute home paths, no symlinks, valid frontmatter, no duplicate names.
- CI: build + drift check and an offline Pi CLI load test of pi/core.
- Release: VERSION, package.json and pi/core/package.json at 2.2.2, CHANGELOG, tag-triggered release verification, and a two-week cadence in CONTRIBUTING.md.

pi/core: 123 of 293 skills and 24 of 94 commands; 35,006 characters of skill description text.
2026-09-29 20:24:05 -05:00

391 lines
16 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Regenerate pi/core/ deterministically from manifests/pi-core.json.
*
* The profile is a curated, Pi-native, skills+prompts-only package:
* pi/core/package.json - { name: "ecc-pi-core", version: <root VERSION>, license: MIT,
* keywords: ["pi-package", "skills"],
* pi: { skills: ["./skills"], prompts: ["./commands"] } }
* pi/core/LICENSE - copy of the root LICENSE
* pi/core/README.md - short generated overview
* pi/core/CURATION.md - every excluded skill/command with its reason
* pi/core/skills/ - curated skill directories (council renamed to ecc-council)
* pi/core/commands/ - curated prompt command files
*
* Safety checks (build fails if violated; semantics documented in
* manifests/pi-core.json safety.semantics):
* - no callable http(s) endpoints (documentation links allowed via the host
* allowlist in manifests/pi-core.json, plus localhost/example placeholders
* and non-FQDN internal hostnames)
* - no runtime download-and-run forms: pipe-to-shell (curl|sh, wget|sh) or
* fetch-and-run npx (-y/--yes, pkg@version, create-*, degit, skills add);
* skills whose own operation downloads tooling are excluded in the manifest
* - no secrets or tokens
* - no absolute per-user home paths (/Users/..., /home/..., C:\Users\...)
* - no symlinks
* - every SKILL.md has frontmatter with name == directory name and a description
* - no duplicate skill names
*
* Scoped exceptions for incidental mentions (anti-pattern warnings, detection
* examples) live in manifests/pi-core.json safety.scanAllowlist with a reason.
*
* Usage: node scripts/build-pi-core.js [--check]
* --check rebuild and verify pi/core is already up to date (exit 1 on drift)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const MANIFEST_PATH = path.join(ROOT, 'manifests', 'pi-core.json');
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
const PROFILE_DIR = path.join(ROOT, manifest.profile.dir);
const SKILLS_SRC = path.join(ROOT, 'skills');
const COMMANDS_SRC = path.join(ROOT, 'commands');
const VERSION = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf8').trim();
const CHECK_MODE = process.argv.includes('--check');
const violations = [];
function fail(msg) {
violations.push(msg);
}
// ---------- partition completeness -----------------------------------------
// Every on-disk skill dir and command file must be classified exactly once in
// the manifest, so new content cannot silently bypass curation.
{
const onDiskSkills = fs.readdirSync(SKILLS_SRC, { withFileTypes: true })
.filter(e => e.isDirectory()).map(e => e.name);
const known = new Set([...manifest.skills.include, ...Object.keys(manifest.skills.exclude)]);
for (const dir of onDiskSkills) {
if (!known.has(dir)) fail(`skills/${dir} is not classified in manifests/pi-core.json`);
}
for (const name of known) {
if (!onDiskSkills.includes(name)) fail(`manifests/pi-core.json references missing skills/${name}`);
}
const onDiskCommands = fs.readdirSync(COMMANDS_SRC).filter(f => f.endsWith('.md'));
const knownCmd = new Set([...manifest.commands.include, ...Object.keys(manifest.commands.exclude)]);
for (const f of onDiskCommands) {
if (!knownCmd.has(f)) fail(`commands/${f} is not classified in manifests/pi-core.json`);
}
for (const f of knownCmd) {
if (!onDiskCommands.includes(f)) fail(`manifests/pi-core.json references missing commands/${f}`);
}
}
// ---------- deterministic copy ----------------------------------------------
/** Recursively list files under dir, sorted; reject symlinks. */
function listFiles(dir, base) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const full = path.join(dir, entry.name);
const rel = base ? `${base}/${entry.name}` : entry.name;
if (entry.isSymbolicLink()) {
fail(`symlink found in profile source: ${rel}`);
} else if (entry.isDirectory()) {
out.push(...listFiles(full, rel));
} else if (entry.isFile()) {
out.push({ full, rel });
}
}
return out;
}
function copyFile(src, dest) {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
}
/** Parse YAML frontmatter minimally: returns { name, description } or null. */
function parseFrontmatter(text) {
const m = text.replace(/^\uFEFF/, '').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!m) return null;
const name = (m[1].match(/^name:\s*["']?(.+?)["']?\s*$/m) || [])[1];
const description = (m[1].match(/^description:\s*["']?([\s\S]+?)["']?\s*$/m) || [])[1];
return { name: name || '', description: description || '', raw: m[1] };
}
// ---------- rebuild pi/core --------------------------------------------------
const tmpDir = path.join(ROOT, 'pi', '.core-build-tmp');
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir, { recursive: true });
const rename = manifest.skills.rename || {};
const skillDescriptions = [];
const seenNames = new Map();
for (const dirName of manifest.skills.include) {
const srcDir = path.join(SKILLS_SRC, dirName);
const outName = rename[dirName] || dirName;
const destDir = path.join(tmpDir, 'skills', outName);
const skillMd = path.join(srcDir, 'SKILL.md');
if (!fs.existsSync(skillMd)) {
fail(`skills/${dirName}/SKILL.md is missing`);
continue;
}
const text = fs.readFileSync(skillMd, 'utf8');
const fm = parseFrontmatter(text);
if (!fm) {
fail(`skills/${dirName}/SKILL.md has no parseable frontmatter`);
} else {
if (fm.name !== dirName) fail(`skills/${dirName}/SKILL.md frontmatter name "${fm.name}" != directory name`);
if (!fm.description) fail(`skills/${dirName}/SKILL.md frontmatter has no description`);
skillDescriptions.push(fm.description);
if (seenNames.has(outName)) fail(`duplicate skill name in profile: ${outName} (skills/${dirName} and skills/${seenNames.get(outName)})`);
seenNames.set(outName, dirName);
}
for (const f of listFiles(srcDir, '')) {
let content = null;
if (rename[dirName] && f.rel === 'SKILL.md') {
// Rename the skill inside pi/core only; the root skill keeps its name.
content = fs.readFileSync(f.full, 'utf8')
.replace(/^name:\s*["']?[^\n"']+["']?\s*$/m, `name: ${outName}`);
fs.mkdirSync(path.dirname(path.join(destDir, f.rel)), { recursive: true });
fs.writeFileSync(path.join(destDir, f.rel), content);
} else {
copyFile(f.full, path.join(destDir, f.rel));
}
}
}
for (const file of manifest.commands.include) {
copyFile(path.join(COMMANDS_SRC, file), path.join(tmpDir, 'commands', file));
}
copyFile(path.join(ROOT, 'LICENSE'), path.join(tmpDir, 'LICENSE'));
const profilePackage = {
name: manifest.profile.packageName,
version: VERSION,
license: manifest.profile.license,
keywords: manifest.profile.keywords,
pi: { skills: ['./skills'], prompts: ['./commands'] },
};
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(profilePackage, null, 2) + '\n');
const skillCount = manifest.skills.include.length;
const commandCount = manifest.commands.include.length;
const descChars = skillDescriptions.reduce((n, d) => n + d.length, 0);
fs.writeFileSync(path.join(tmpDir, 'README.md'), `# ecc-pi-core
A curated, Pi-native profile of ECC (Everything Claude Code): ${skillCount} portable
engineering skills and ${commandCount} pure prompt-workflow commands, with no extensions,
no hooks, no runtime downloads, and no network or SaaS dependencies.
## Contents
- \`skills/\` - language, framework, testing/TDD, code review, security review,
planning, refactoring, docs, and git/PR workflow skills.
- \`commands/\` - prompt commands that are pure prompt workflows.
- \`CURATION.md\` - every excluded skill and command with its reason.
## Use
Copy this directory into your project (or pin a release tarball) and load it with the
Pi coding agent:
\`\`\`sh
pi --no-extensions --extension pi/core
\`\`\`
Offline load test (as run in CI):
\`\`\`sh
PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \\
--extension pi/core </dev/null >/dev/null
\`\`\`
## Regenerate
\`pi/core\` is generated from \`manifests/pi-core.json\` and committed so release
tarballs contain it verbatim. After changing the manifest or any included source
content, run:
\`\`\`sh
node scripts/build-pi-core.js
\`\`\`
and commit the result. CI verifies the committed profile is up to date.
`);
{
const lines = [];
lines.push('# Curation');
lines.push('');
lines.push(`pi/core includes ${skillCount} of ${Object.keys(manifest.skills.exclude).length + skillCount} skills ` +
`and ${commandCount} of ${Object.keys(manifest.commands.exclude).length + commandCount} commands from the root of ECC.`);
lines.push('Everything excluded is listed here with its reason.');
lines.push('');
lines.push('## Rules');
lines.push('');
lines.push('Include: ' + manifest.curationRules.include.join('; ') + '.');
lines.push('');
lines.push('Exclude anything that:');
for (const rule of manifest.curationRules.exclude) lines.push(`- ${rule}`);
lines.push('');
lines.push('## Excluded skills');
lines.push('');
lines.push('| Skill | Reason |');
lines.push('|---|---|');
for (const [name, reason] of Object.entries(manifest.skills.exclude)) {
lines.push(`| \`${name}\` | ${reason.replace(/\|/g, '\\|')} |`);
}
lines.push('');
lines.push('## Excluded commands');
lines.push('');
lines.push('| Command | Reason |');
lines.push('|---|---|');
for (const [file, reason] of Object.entries(manifest.commands.exclude)) {
lines.push(`| \`${file.replace(/\.md$/, '')}\` | ${reason.replace(/\|/g, '\\|')} |`);
}
lines.push('');
lines.push('## Renames');
lines.push('');
for (const [from, to] of Object.entries(rename)) {
lines.push(`- \`${from}\` is shipped as \`${to}\` inside pi/core (the root skill keeps its original name).`);
}
lines.push('');
fs.writeFileSync(path.join(tmpDir, 'CURATION.md'), lines.join('\n'));
}
// ---------- safety scans -----------------------------------------------------
//
// Scanner semantics (mirrors the curation rules in manifests/pi-core.json):
// - URLs: documentation links are allowed via safety.urlAllowlistHosts.
// Placeholder hosts (example.com/org/net and subdomains) and non-FQDN
// internal hostnames (localhost, docker service names like "api" or "db")
// are always allowed; they are not callable endpoints.
// - Runtime downloads: pipe-to-shell (curl|sh, wget|sh) and fetch-and-run
// npx forms (-y/--yes, pkg@version, create-*, degit, "skills add") fail.
// Local-first `npx <tool>` (jest, tsc, playwright, prisma, ...) and
// standard project dependency installation (pip install <dep>, npm i) are
// the reader's own project workflow, not the profile downloading code to
// run itself, so they are allowed; skills whose own operation downloads
// tooling are excluded in the manifest instead (see CURATION.md).
// - Home paths: absolute per-user paths (/Users/..., /home/..., C:\Users\...)
// fail. Portable tilde references (~/.cache/...) are user-agnostic and OK.
const defaultHosts = new Set(manifest.safety.defaultAllowedHosts || []);
const allowHosts = new Set([...(manifest.safety.urlAllowlistHosts || []), ...defaultHosts]);
const scanAllowlist = manifest.safety.scanAllowlist || [];
const PLACEHOLDER_SUFFIXES = ['.example.com', '.example.org', '.example.net'];
function isAllowlisted(relPath, line) {
return scanAllowlist.some(e => relPath === e.path && line.includes(e.contains));
}
function hostAllowed(hostname) {
if (!hostname) return false;
if (allowHosts.has(hostname)) return true;
if (!hostname.includes('.')) return true; // localhost, docker service names, placeholders
return PLACEHOLDER_SUFFIXES.some(s => hostname.endsWith(s));
}
const URL_RE = /https?:\/\/([A-Za-z0-9._-]+)(?::\d+)?[^\s)\]"'`<>}]*/g;
const SECRET_RES = [
{ re: /AKIA[0-9A-Z]{16}/, label: 'AWS access key' },
{ re: /ghp_[A-Za-z0-9]{20,}/, label: 'GitHub PAT' },
{ re: /github_pat_[A-Za-z0-9_]{22,}/, label: 'GitHub fine-grained PAT' },
{ re: /sk-[A-Za-z0-9_-]{20,}/, label: 'OpenAI-style key' },
{ re: /xox[baprs]-[A-Za-z0-9-]{10,}/, label: 'Slack token' },
{ re: /AIza[0-9A-Za-z_-]{35}/, label: 'Google API key' },
{ re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/, label: 'private key block' },
{ re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, label: 'JWT' },
];
const INSTALL_RES = [
{ re: /\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/, label: 'pipe-to-shell (curl|sh / wget|sh)' },
{ re: /\bnpx\s+(?:-y\b|--yes\b)/, label: 'npx -y/--yes (fetch-and-run)' },
{ re: /\bnpx\s+(?:--\S+\s+)*[A-Za-z@][^\s]*@\d/, label: 'npx pkg@version (fetch-and-run)' },
{ re: /\bnpx\s+(?:--\S+\s+)*(?:create-[a-z-]+|degit\b|skills\s+add\b)/, label: 'npx scaffold/fetch form' },
{ re: /\bpipx\s+install\b/, label: 'pipx install' },
];
const HOME_RES = [
{ re: /\/Users\/[^\s)"'`\]<>|]+/, label: 'macOS home path' },
{ re: /\/home\/[^\s)"'`\]<>|]+/, label: 'Linux home path' },
{ re: /[A-Za-z]:\\Users\\[^\s)"'`\]<>|]+/, label: 'Windows home path' },
];
const GENERATED_FILES = new Set(['package.json', 'README.md', 'CURATION.md']);
function scanTextFile(relPath, text) {
if (GENERATED_FILES.has(relPath)) return; // generated from the manifest itself
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const at = `${relPath}:${i + 1}`;
const allowlisted = isAllowlisted(relPath, line);
for (const m of line.matchAll(URL_RE)) {
if (!hostAllowed(m[1]) && !allowlisted) {
fail(`${at}: non-allowlisted URL host ${m[1]} (${m[0].slice(0, 100)})`);
}
}
if (allowlisted) continue;
for (const { re, label } of SECRET_RES) {
if (re.test(line)) fail(`${at}: possible secret (${label})`);
}
for (const { re, label } of INSTALL_RES) {
if (re.test(line)) fail(`${at}: runtime download pattern (${label})`);
}
for (const { re, label } of HOME_RES) {
if (re.test(line)) fail(`${at}: absolute home path (${label})`);
}
}
}
for (const f of listFiles(tmpDir, '')) {
const rel = f.rel;
const buf = fs.readFileSync(f.full);
if (buf.includes(0)) continue; // binary file: nothing textual to scan
scanTextFile(rel, buf.toString('utf8'));
}
// ---------- emit report -------------------------------------------------------
const report = [
`pi/core: ${skillCount} skills, ${commandCount} commands`,
`skill description text: ${descChars} characters (target: under ~40000)`,
];
if (descChars > 42000) fail(`skill description text too large: ${descChars} characters`);
if (violations.length) {
fs.rmSync(tmpDir, { recursive: true, force: true });
console.error('pi/core build FAILED:');
for (const v of violations) console.error(' - ' + v);
console.error(report.join('\n'));
process.exit(1);
}
if (CHECK_MODE) {
// Compare tmpDir against the committed profile without touching it.
const committed = listFiles(PROFILE_DIR, '').map(f => f.rel).sort();
const built = listFiles(tmpDir, '').map(f => f.rel).sort();
let drift = false;
if (committed.join('\n') !== built.join('\n')) {
console.error('pi/core file list drift:');
const cSet = new Set(committed), bSet = new Set(built);
for (const f of committed) if (!bSet.has(f)) console.error(' only committed: ' + f);
for (const f of built) if (!cSet.has(f)) console.error(' only built: ' + f);
drift = true;
} else {
for (const rel of committed) {
const a = fs.readFileSync(path.join(PROFILE_DIR, rel));
const b = fs.readFileSync(path.join(tmpDir, rel));
if (!a.equals(b)) { console.error('pi/core content drift: ' + rel); drift = true; }
}
}
fs.rmSync(tmpDir, { recursive: true, force: true });
if (drift) {
console.error('Run: node scripts/build-pi-core.js');
process.exit(1);
}
console.log('pi/core is up to date.');
console.log(report.join('\n'));
process.exit(0);
}
fs.rmSync(PROFILE_DIR, { recursive: true, force: true });
fs.renameSync(tmpDir, PROFILE_DIR);
console.log(report.join('\n'));
console.log(`wrote ${manifest.profile.dir}/`);