chore(root): refresh agent skills and docs (#1835)

This commit is contained in:
rahim
2026-07-27 16:21:40 -07:00
committed by GitHub
parent b4b30b0a01
commit fd4d2662ea
206 changed files with 2199 additions and 22577 deletions
+374 -5
View File
@@ -10,11 +10,14 @@
* 3. Root tsconfig references — every composite project is referenced
* 4. Package metadata — non-private packages have required fields
* 5. Release-please config — every versioned package is registered
* 6. Define imports — no bare side-effect imports from relative paths
* 7. i18n locales — tag lists match locale files and generated stubs
* 6. Bundled docs — package publishing wires include generated docs
* 7. Define imports — no bare side-effect imports from relative paths
* 8. i18n locales — tag lists match locale files and generated stubs
* 9. Agent context — portable skill metadata, compatibility imports, and budgets
* 10. Internal records — organized design docs, frontmatter, and lifecycle status
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
@@ -325,7 +328,7 @@ function checkDefineImports() {
return { ok: warnings.length === 0, warnings };
}
// ── Check 7: i18n locale consistency ─────────────────────────────────────────
// ── Check 8: i18n locale consistency ─────────────────────────────────────────
const GENERATED_I18N_HEADER = '/** Generated by packages/core/scripts/generate-i18n-locales.ts — do not edit. */';
@@ -451,6 +454,370 @@ function checkI18nLocales() {
return { ok: warnings.length === 0, warnings };
}
// ── Check 9: Agent context consistency ─────────────────────────────────────
// These are conservative repository budgets, below the host-level ceilings.
// Token counts are estimates using four UTF-8 bytes per token; the byte limit
// is the enforceable value and avoids adding a tokenizer dependency.
const AGENT_DOC_MAX_LINES = 200;
const AGENT_DOC_MAX_BYTES = 12_000;
const AGENT_CHAIN_MAX_BYTES = 24_000;
const SKILL_MAX_LINES = 200;
const SKILL_MAX_BYTES = 10_000;
const SKILL_RESOURCE_MAX_LINES = 500;
const SKILL_RESOURCE_MAX_BYTES = 20_000;
const RESTORED_SPF_RESOURCE_MAX_LINES = 1_200;
const RESTORED_SPF_RESOURCE_MAX_BYTES = 60_000;
const SKILL_METADATA_MAX_BYTES = 6_000;
const RESTORED_SPF_SKILLS = new Set([
'change-spf-behavior',
'create-spf-behavior',
'document-spf-feature',
'document-spf-use-case',
'implement-spf-feature',
'implement-spf-use-case',
]);
const PORTABLE_SKILL_FIELDS = new Set(['name', 'description']);
const SKILL_ACTIONS = new Set([
'build',
'change',
'commit',
'create',
'design',
'document',
'implement',
'investigate',
'maintain',
'migrate',
'review',
'write',
]);
function lineCount(text) {
return text === '' ? 0 : text.split(/\r?\n/).length;
}
function estimatedTokens(bytes) {
return Math.ceil(bytes / 4);
}
function relativePath(path) {
return path.slice(ROOT.length + 1);
}
function listFiles(dir, predicate, results = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (['.git', '.agents', '.opencode', 'node_modules', 'dist', 'coverage'].includes(entry.name)) {
continue;
}
const full = join(dir, entry.name);
if (entry.isDirectory()) {
listFiles(full, predicate, results);
} else if (predicate(full)) {
results.push(full);
}
}
return results;
}
function checkFileBudget(path, maxLines, maxBytes, warnings) {
const source = readText(path);
const lines = lineCount(source);
const bytes = Buffer.byteLength(source);
const relative = relativePath(path);
if (lines > maxLines) {
warnings.push(`${relative}: ${lines} lines exceeds ${maxLines}`);
}
if (bytes > maxBytes) {
warnings.push(
`${relative}: ~${estimatedTokens(bytes)} tokens (${bytes} bytes) exceeds ~${estimatedTokens(maxBytes)} tokens`
);
}
}
function checkAgentContext() {
const warnings = [];
const agentDocs = listFiles(ROOT, (path) => path.endsWith('/AGENTS.md'));
const claudeDocs = listFiles(ROOT, (path) => path.endsWith('/CLAUDE.md'));
const gitignoreRules = new Set(
readText(join(ROOT, '.gitignore'))
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'))
);
for (const rule of ['/.claude/skills', '/.claude/plans', '/.opencode']) {
if (!gitignoreRules.has(rule)) {
warnings.push(`.gitignore: missing generated agent path ${rule}`);
}
}
for (const rule of ['/.agents/skills', '/.agents/skills/', '.agents/skills', '.agents/skills/']) {
if (gitignoreRules.has(rule)) {
warnings.push(`.gitignore: canonical .agents/skills catalog must not be ignored by ${rule}`);
}
}
if (!agentDocs.includes(join(ROOT, 'AGENTS.md'))) {
warnings.push('Missing canonical root AGENTS.md');
}
for (const path of agentDocs) {
checkFileBudget(path, AGENT_DOC_MAX_LINES, AGENT_DOC_MAX_BYTES, warnings);
const directory = dirname(path);
const chainBytes = agentDocs
.filter((candidate) => {
const candidateDirectory = dirname(candidate);
return directory === candidateDirectory || directory.startsWith(`${candidateDirectory}${sep}`);
})
.reduce((total, candidate) => total + Buffer.byteLength(readText(candidate)), 0);
if (chainBytes > AGENT_CHAIN_MAX_BYTES) {
warnings.push(
`${relativePath(path)} chain: ~${estimatedTokens(chainBytes)} tokens (${chainBytes} bytes) exceeds ` +
`~${estimatedTokens(AGENT_CHAIN_MAX_BYTES)} tokens`
);
}
}
for (const path of claudeDocs) {
const siblingAgents = join(dirname(path), 'AGENTS.md');
const relative = relativePath(path);
if (!existsSync(siblingAgents)) {
warnings.push(`${relative}: missing sibling AGENTS.md`);
}
if (readText(path).trim() !== '@AGENTS.md') {
warnings.push(`${relative}: must contain only \`@AGENTS.md\` to avoid duplicated instructions`);
}
}
const agentsDir = join(ROOT, '.agents');
const skillsDir = join(agentsDir, 'skills');
for (const alias of [join(ROOT, '.claude/skills'), join(ROOT, '.opencode/skills')]) {
if (!existsSync(alias)) {
warnings.push(`${relativePath(alias)}: missing compatibility alias to skills`);
} else if (realpathSync(alias) !== realpathSync(skillsDir)) {
warnings.push(`${relativePath(alias)}: must resolve to skills`);
}
}
const plansDir = join(agentsDir, 'plans');
const claudePlans = join(ROOT, '.claude/plans');
if (!existsSync(claudePlans)) {
warnings.push('.claude/plans: missing compatibility alias to .agents/plans');
} else if (realpathSync(claudePlans) !== realpathSync(plansDir)) {
warnings.push('.claude/plans: must resolve to .agents/plans');
}
const canonicalSkillDirs = [];
const skillNames = new Set();
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
warnings.push(`.agents/skills/${entry.name}: only skill directories are allowed at the catalog root`);
continue;
}
const skillDir = join(skillsDir, entry.name);
if (!existsSync(join(skillDir, 'SKILL.md'))) {
warnings.push(`.agents/skills/${entry.name}: missing SKILL.md`);
continue;
}
skillNames.add(entry.name);
canonicalSkillDirs.push(skillDir);
}
const canonicalSkillFiles = new Set(canonicalSkillDirs.map((dir) => join(dir, 'SKILL.md')));
for (const path of listFiles(skillsDir, (path) => path.endsWith('/SKILL.md'))) {
if (!canonicalSkillFiles.has(path)) {
warnings.push(`${relativePath(path)}: skills must be direct children of .agents/skills/`);
}
}
const skillFiles = canonicalSkillDirs.map((dir) => join(dir, 'SKILL.md'));
let metadataBytes = 0;
for (const path of skillFiles) {
checkFileBudget(path, SKILL_MAX_LINES, SKILL_MAX_BYTES, warnings);
const source = readText(path);
const relative = relativePath(path);
const frontmatterMatch = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!frontmatterMatch) {
warnings.push(`${relative}: missing YAML frontmatter`);
continue;
}
const frontmatter = frontmatterMatch[1];
metadataBytes += Buffer.byteLength(frontmatter);
const fields = [...frontmatter.matchAll(/^([A-Za-z][A-Za-z0-9-]*):/gm)].map((match) => match[1]);
for (const field of fields) {
if (!PORTABLE_SKILL_FIELDS.has(field)) {
warnings.push(`${relative}: non-portable frontmatter field "${field}"`);
}
}
const name = frontmatter
.match(/^name:\s*([^\r\n]+)$/m)?.[1]
.trim()
.replace(/^['"]|['"]$/g, '');
const directoryName = relative.split('/').at(-2);
if (!name) {
warnings.push(`${relative}: missing skill name`);
} else if (name !== directoryName) {
warnings.push(`${relative}: name "${name}" must match directory "${directoryName}"`);
} else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) {
warnings.push(`${relative}: invalid Agent Skills name "${name}"`);
} else if (!SKILL_ACTIONS.has(name.split('-')[0])) {
warnings.push(`${relative}: skill name "${name}" must start with a clear action verb`);
}
const description = frontmatter.match(/^description:\s*(.+)$/m)?.[1].trim();
if (!description) {
warnings.push(`${relative}: missing skill description`);
} else if (description.length > 1024 || !/\bUse (?:for|when)\b/.test(description)) {
warnings.push(`${relative}: description must concisely say what the skill does and when to use it`);
}
if (!/^## Example\r?$/m.test(source) || !/^Input: /m.test(source) || !/^Output: /m.test(source)) {
warnings.push(`${relative}: include one compact Example with Input and Output`);
}
}
if (metadataBytes > SKILL_METADATA_MAX_BYTES) {
warnings.push(
`.agents/skills metadata: ~${estimatedTokens(metadataBytes)} tokens (${metadataBytes} bytes) exceeds ` +
`~${estimatedTokens(SKILL_METADATA_MAX_BYTES)} tokens`
);
}
for (const skillDir of canonicalSkillDirs) {
const owner = relativePath(skillDir).split('/').at(-1);
const restoredSpfWorkflow = RESTORED_SPF_SKILLS.has(owner);
const resourceMaxLines = restoredSpfWorkflow ? RESTORED_SPF_RESOURCE_MAX_LINES : SKILL_RESOURCE_MAX_LINES;
const resourceMaxBytes = restoredSpfWorkflow ? RESTORED_SPF_RESOURCE_MAX_BYTES : SKILL_RESOURCE_MAX_BYTES;
for (const path of listFiles(skillDir, (path) => path.endsWith('.md') && !path.endsWith('/SKILL.md'))) {
checkFileBudget(path, resourceMaxLines, resourceMaxBytes, warnings);
}
for (const path of listFiles(skillDir, (path) => path.endsWith('.md'))) {
const source = readText(path);
for (const name of skillNames) {
if (name !== owner && source.includes(`\`${name}\``)) {
warnings.push(`${relativePath(path)}: must not explicitly load or route to sibling skill "${name}"`);
}
}
}
}
return { ok: warnings.length === 0, warnings };
}
// ── Check 10: Internal record consistency ──────────────────────────────────
const DESIGN_STATUSES = new Set(['draft', 'decided', 'active', 'partial', 'implemented', 'superseded', 'reference']);
const INTERNAL_RECORD_MAX_LINES = 160;
const RESTORED_SPF_RECORD_MAX_LINES = 1_000;
function recordFrontmatter(path, warnings) {
const relative = relativePath(path);
const match = readText(path).match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) {
warnings.push(`${relative}: missing YAML frontmatter`);
return undefined;
}
return Object.fromEntries(
[...match[1].matchAll(/^([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\s*$/gm)].map((field) => [field[1], field[2]])
);
}
function checkLocalMarkdownLinks(path, warnings) {
const source = readText(path);
for (const match of source.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
let target = match[1].trim();
if (target.startsWith('<') && target.endsWith('>')) target = target.slice(1, -1);
if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue;
target = target
.split(/\s+["']/)[0]
.split('#')[0]
.split('?')[0];
if (!target) continue;
try {
target = decodeURIComponent(target);
} catch {
warnings.push(`${relativePath(path)}: invalid encoded Markdown link "${match[1]}"`);
continue;
}
const resolved = target.startsWith('/') ? join(ROOT, target.slice(1)) : resolve(dirname(path), target);
if (!existsSync(resolved)) {
warnings.push(`${relativePath(path)}: broken local Markdown link "${match[1]}"`);
}
}
}
function checkInternalRecords() {
const warnings = [];
const designDir = join(ROOT, 'internal/design');
const designReadme = join(designDir, 'README.md');
for (const entry of readdirSync(designDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.md') && join(designDir, entry.name) !== designReadme) {
warnings.push(`internal/design/${entry.name}: place design records in an area directory`);
}
}
for (const path of listFiles(designDir, (path) => path.endsWith('.md'))) {
if (path === designReadme) continue;
const recordMaxLines = path.startsWith(`${join(designDir, 'spf')}${sep}`)
? RESTORED_SPF_RECORD_MAX_LINES
: INTERNAL_RECORD_MAX_LINES;
checkFileBudget(path, recordMaxLines, Number.POSITIVE_INFINITY, warnings);
checkLocalMarkdownLinks(path, warnings);
const frontmatter = recordFrontmatter(path, warnings);
if (!frontmatter) continue;
if (!DESIGN_STATUSES.has(frontmatter.status)) {
warnings.push(`${relativePath(path)}: unknown design status "${frontmatter.status ?? 'missing'}"`);
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(frontmatter.date ?? '')) {
warnings.push(`${relativePath(path)}: date must use YYYY-MM-DD`);
}
}
const decisionsDir = join(ROOT, 'internal/decisions');
const decisionsReadme = join(decisionsDir, 'README.md');
for (const entry of readdirSync(decisionsDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.md') && join(decisionsDir, entry.name) !== decisionsReadme) {
warnings.push(`internal/decisions/${entry.name}: place decision records in an area directory`);
}
}
for (const path of listFiles(decisionsDir, (path) => path.endsWith('.md') && !path.endsWith('/README.md'))) {
const recordMaxLines = path.startsWith(`${join(decisionsDir, 'spf')}${sep}`)
? RESTORED_SPF_RECORD_MAX_LINES
: INTERNAL_RECORD_MAX_LINES;
checkFileBudget(path, recordMaxLines, Number.POSITIVE_INFINITY, warnings);
checkLocalMarkdownLinks(path, warnings);
const frontmatter = recordFrontmatter(path, warnings);
if (!frontmatter) continue;
if (frontmatter.status !== 'decided') {
warnings.push(`${relativePath(path)}: tactical decisions must use status "decided"`);
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(frontmatter.date ?? '')) {
warnings.push(`${relativePath(path)}: date must use YYYY-MM-DD`);
}
}
for (const path of listFiles(join(ROOT, 'rfc'), (path) => path.endsWith('.md'))) {
checkFileBudget(path, INTERNAL_RECORD_MAX_LINES, Number.POSITIVE_INFINITY, warnings);
checkLocalMarkdownLinks(path, warnings);
}
return { ok: warnings.length === 0, warnings };
}
// ── Main ────────────────────────────────────────────────────────────────────
const checks = [
@@ -462,6 +829,8 @@ const checks = [
{ name: 'Bundled docs publishing', fn: checkBundledDocs },
{ name: 'Define imports', fn: checkDefineImports },
{ name: 'i18n locales', fn: checkI18nLocales },
{ name: 'Agent context', fn: checkAgentContext },
{ name: 'Internal records', fn: checkInternalRecords },
];
let failed = 0;
+45 -26
View File
@@ -1,37 +1,56 @@
/**
* Creates symlink aliases so that AI coding tools other than Claude Code
* (e.g., OpenCode, Cursor) can discover project instructions.
* Exposes the checked-in, host-neutral skill catalog through client-specific
* discovery paths:
*
* Aliases created:
* .opencode → .claude (directory)
* .agents → .claude (directory)
* AGENTS.md → CLAUDE.md (file)
* .agents/skills/<skill-name>/SKILL.md (source)
* .claude/skills (generated junction)
* .claude/plans (generated junction)
* .opencode (generated junction)
*
* Cross-platform notes:
* - Directory symlinks use 'junction' type, which works on Windows without
* elevated privileges or Developer Mode.
* - File symlinks ('file' type) require elevated privileges or Developer Mode
* on Windows. If creation fails, we log a warning instead of crashing
* `pnpm install`. The alias is optional — the canonical files still work.
* Directory junctions work on Windows without elevated privileges. Failures
* warn instead of breaking `pnpm install`.
*/
import { existsSync, symlinkSync } from 'node:fs';
import { resolve } from 'node:path';
import { lstatSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
const root = resolve(import.meta.dirname, '../..');
const agentsDir = join(root, '.agents');
const skillsDir = join(agentsDir, 'skills');
const aliases = [
{ target: '.claude', path: '.opencode', type: 'junction' },
{ target: '.claude', path: '.agents', type: 'junction' },
{ target: 'CLAUDE.md', path: 'AGENTS.md', type: 'file' },
];
for (const alias of aliases) {
const fullPath = resolve(root, alias.path);
if (existsSync(fullPath)) continue;
function linkState(path) {
try {
symlinkSync(alias.target, fullPath, alias.type);
return lstatSync(path);
} catch {
console.warn(`warning: could not create symlink ${alias.path}${alias.target}`);
return undefined;
}
}
function ensureAlias(relativePath, target) {
const path = resolve(root, relativePath);
const state = linkState(path);
if (state?.isSymbolicLink()) {
try {
if (resolve(dirname(path), readlinkSync(path)) === resolve(target)) return;
} catch {
// Replace a dangling generated link below.
}
unlinkSync(path);
} else if (state) {
console.warn(`warning: refusing to replace non-generated ${relativePath}`);
return;
}
mkdirSync(dirname(path), { recursive: true });
try {
symlinkSync(target, path, 'junction');
} catch {
console.warn(`warning: could not create alias ${relativePath}${target}`);
}
}
mkdirSync(join(agentsDir, 'plans'), { recursive: true });
ensureAlias('.claude/skills', skillsDir);
ensureAlias('.claude/plans', join(agentsDir, 'plans'));
ensureAlias('.opencode', agentsDir);