feat: skill generator with manifest, frontmatter parser and mapping-doc matrix

This commit is contained in:
haylan committed 2026-10-14 11:59:14 +02:00
1 parent b21b974293
commit 4ce4466a97
63 files changed
+4552 -42

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
// Regenerates the OpenCode artifacts from the pristine upstream snapshot:
//
// node scripts/generate.ts
//
// Inputs: upstream/skills/** + .opencode/state/classification.json
// Outputs: skills/<name>/** (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 <!-- matrix:start/end --> 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<string>()
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<string, string> = {
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 = "<!-- matrix:start -->"
const end = "<!-- matrix: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)
}
}