// Regenerates the OpenCode artifacts from the pristine upstream snapshot: // // node scripts/generate.ts // // Inputs: upstream/skills/** + .opencode/state/classification.json // Outputs: skills//** (verbatim, minus agents/openai.yaml; only skills whose // classification artifacts include "skill:verbatim") // manifest.json (plugin-injection manifest: id/name/description/autoinvoke/path) // docs/skill-mapping.md matrix section (between markers) // // Deterministic: same inputs produce byte-identical outputs, so the nightly git diff is the // affected-set report. Never hand-edit skills/ or the matrix section. import { cpSync, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs" import path from "node:path" import { fileURLToPath } from "node:url" import { isUserInvoked, parseFrontmatter } from "./frontmatter.ts" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") export type ClassificationEntry = { name: string bucket: string classification: string user_invoked: boolean artifacts: string[] depends_on: string[] status: string rationale: string } export type ManifestEntry = { id: string name: string description: string autoinvoke: boolean path: string } export function loadClassification(file: string): ClassificationEntry[] { const doc = JSON.parse(readFileSync(file, "utf8")) return doc.skills as ClassificationEntry[] } function copySkillDir(from: string, to: string) { rmSync(to, { recursive: true, force: true }) cpSync(from, to, { recursive: true, filter: (src) => { // strip Codex UI metadata dir; everything else ships verbatim const rel = path.relative(from, src) return rel !== "agents" && !rel.startsWith("agents" + path.sep) }, }) } export function buildSkills(opts: { upstreamSkills: string entries: ClassificationEntry[] skillsOut: string }): ManifestEntry[] { const vendored = opts.entries.filter((e) => e.artifacts.includes("skill:verbatim")) const seen = new Set() const manifest: ManifestEntry[] = [] rmSync(opts.skillsOut, { recursive: true, force: true }) for (const entry of vendored) { if (seen.has(entry.name)) throw new Error(`duplicate skill name across buckets: ${entry.name}`) seen.add(entry.name) const from = path.join(opts.upstreamSkills, entry.bucket, entry.name) const skillMd = path.join(from, "SKILL.md") if (!existsSync(skillMd)) { throw new Error(`classified skill missing upstream: ${entry.bucket}/${entry.name}`) } const to = path.join(opts.skillsOut, entry.name) copySkillDir(from, to) const { frontmatter } = parseFrontmatter(readFileSync(skillMd, "utf8")) const name = frontmatter.name || entry.name const description = frontmatter.description if (!frontmatter.name || !description) { throw new Error(`${entry.name}: SKILL.md frontmatter needs name and description`) } manifest.push({ id: entry.name, name, description, autoinvoke: !isUserInvoked(frontmatter), path: `skills/${entry.name}/SKILL.md`, }) } // fail on upstream skills nobody classified (sync-upstream normally adds needs-review entries) for (const bucket of existsSync(opts.upstreamSkills) ? readdirSync(opts.upstreamSkills) : []) { const bucketPath = path.join(opts.upstreamSkills, bucket) if (!statSync(bucketPath).isDirectory()) continue for (const name of readdirSync(bucketPath)) { if (!existsSync(path.join(bucketPath, name, "SKILL.md"))) continue if (!opts.entries.some((e) => e.name === name)) { throw new Error(`unclassified upstream skill: ${bucket}/${name} — run sync + review`) } } } manifest.sort((a, b) => a.id.localeCompare(b.id)) return manifest } const BUCKET_SHORT: Record = { engineering: "eng", productivity: "prod", misc: "misc", "in-progress": "in-prog", deprecated: "dep", } const cell = (value: string) => value.replace(/\|/g, "\\|") export function renderMatrix(entries: ClassificationEntry[]): string { const lines = [ "| Upstream skill | Bucket | DMI | Classification | Artifacts | Depends on | Status | Rationale |", "| --- | --- | --- | --- | --- | --- | --- | --- |", ] for (const e of entries) { lines.push( `| ${e.name} | ${BUCKET_SHORT[e.bucket] ?? e.bucket} | ${e.user_invoked ? "✓" : ""} | ` + `${e.classification} | ${cell(e.artifacts.join(", "))} | ${cell(e.depends_on.join(", "))} | ` + `${e.status} | ${cell(e.rationale)} |`, ) } return lines.join("\n") } export function updateDocMatrix(docPath: string, matrix: string): void { const doc = readFileSync(docPath, "utf8") const start = "" const end = "" const i = doc.indexOf(start) const j = doc.indexOf(end) if (i === -1 || j === -1 || j < i) throw new Error(`${docPath}: missing matrix markers`) writeFileSync(docPath, `${doc.slice(0, i + start.length)}\n\n${matrix}\n\n${doc.slice(j)}`) } function main() { const classificationPath = path.join(root, ".opencode/state/classification.json") const docPath = path.join(root, "docs/skill-mapping.md") const entries = loadClassification(classificationPath) const manifest = buildSkills({ upstreamSkills: path.join(root, "upstream/skills"), entries, skillsOut: path.join(root, "skills"), }) writeFileSync(path.join(root, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n") updateDocMatrix(docPath, renderMatrix(entries)) const needsReview = entries.filter((e) => e.status === "needs-review").map((e) => e.name) console.log( `generated ${manifest.length} skills (${manifest.filter((m) => !m.autoinvoke).length} hidden from model), manifest.json, matrix in docs/skill-mapping.md` + (needsReview.length ? `\nWARNING needs-review: ${needsReview.join(", ")}` : ""), ) } if (process.argv[1] === fileURLToPath(import.meta.url)) { try { main() } catch (err) { console.error((err as Error).message) process.exit(1) } }