feat: skill generator with manifest, frontmatter parser and mapping-doc matrix
This commit is contained in:
1 parent
b21b974293
commit
4ce4466a97
63 files changed
+4552
-42
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user