diff --git a/src/agent/clis/index.ts b/src/agent/clis/index.ts index 57b91c2f..1d9a7059 100644 --- a/src/agent/clis/index.ts +++ b/src/agent/clis/index.ts @@ -561,7 +561,7 @@ function optimizeSection(opts: OptimizeSectionOptions): Promise { // ── Main orchestrator ──────────────────────────────────────────────── export async function optimizeDocs(opts: OptimizeDocsOptions): Promise { - const { packageName, skillDir, model = 'sonnet', version, hasGithub, hasReleases, hasChangelog, docFiles, docsType, hasShippedDocs, onProgress, timeout = 180000, debug, noCache, sections, customPrompt, features, pkgFiles } = opts + const { packageName, skillDir, model = 'sonnet', version, hasGithub, hasReleases, hasChangelog, docFiles, docsType, hasShippedDocs, onProgress, timeout = 180000, debug, noCache, sections, customPrompt, features, pkgFiles, overheadLines } = opts const selectedSections = sections ?? ['api-changes', 'best-practices'] as SkillSection[] @@ -580,6 +580,7 @@ export async function optimizeDocs(opts: OptimizeDocsOptions): Promise` or `:L:`). If you cannot cite a specific location in a release, changelog entry, or migration doc, do NOT include the item', '- **Recency:** Only include changes from the current major version and the previous→current migration. Exclude changes from older major versions entirely — users already migrated past them', '- Focus on APIs that CHANGED, not general conventions or gotchas', diff --git a/src/agent/prompts/optional/best-practices.ts b/src/agent/prompts/optional/best-practices.ts index 621604cf..90ebc12f 100644 --- a/src/agent/prompts/optional/best-practices.ts +++ b/src/agent/prompts/optional/best-practices.ts @@ -3,7 +3,7 @@ import { resolveSkilldCommand } from '../../../core/shared.ts' import { maxItems, maxLines, releaseBoost } from './budget.ts' import { checkAbsolutePaths, checkLineCount, checkSourceCoverage, checkSourcePaths, checkSparseness } from './validate.ts' -export function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles, features, enabledSectionCount, releaseCount, version }: SectionContext): PromptSection { +export function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles, features, enabledSectionCount, releaseCount, version, overheadLines }: SectionContext): PromptSection { const [,, minor] = version?.match(/^(\d+)\.(\d+)/) ?? [] // Dampened boost — best practices are less directly tied to releases than API changes const rawBoost = releaseBoost(releaseCount, minor ? Number(minor) : undefined) @@ -35,7 +35,7 @@ export function bestPracticesSection({ packageName, hasIssues, hasDiscussions, h referenceWeights.push({ name: 'Changelog', path: `./.skilld/${hasChangelog}`, score: 3, useFor: 'Only for new patterns introduced in recent versions' }) } - const bpMaxLines = maxLines(80, Math.round(150 * boost), enabledSectionCount) + const bpMaxLines = maxLines(100, Math.round(250 * boost), enabledSectionCount, overheadLines) return { referenceWeights, @@ -86,7 +86,7 @@ const client = createX({ retryDelay: attempt => Math.min(1000 * 2 ** attempt, 30 Each item: markdown list item (-) + ${packageName}-specific pattern + why it's preferred + \`[source](./.skilld/...#section)\` link. **Prefer concise descriptions over inline code** — the source link points the agent to full examples in the docs. Only add a code block when the pattern genuinely cannot be understood from the description alone (e.g., non-obvious syntax, multi-step wiring). Most items should be description + source link only. All source links MUST use \`./.skilld/\` prefix and include a **section anchor** (\`#heading-slug\`) or **line reference** (\`:L\` or \`:L:\`) to pinpoint the exact location. Do NOT use emoji — use plain text markers only.`, rules: [ - `- **${maxItems(4, Math.round(10 * boost), enabledSectionCount)} best practice items**`, + `- **${maxItems(6, Math.round(15 * boost), enabledSectionCount)} best practice items**`, `- **MAX ${bpMaxLines} lines** for best practices section`, '- **Every item MUST have a `[source](./.skilld/...#section)` link** with a section anchor (`#heading-slug`) or line reference (`:L` or `:L:`). If you cannot cite a specific location in a reference file, do NOT include the item — unsourced items risk hallucination and will be rejected', '- **Minimize inline code.** Most items should be description + source link only. The source file contains full examples the agent can read. Only add a code block when the pattern is unintuitable from the description (non-obvious syntax, surprising argument order, multi-step wiring). Aim for at most 1 in 4 items having a code block', diff --git a/src/agent/prompts/optional/budget.ts b/src/agent/prompts/optional/budget.ts index 84479b29..1387bfc1 100644 --- a/src/agent/prompts/optional/budget.ts +++ b/src/agent/prompts/optional/budget.ts @@ -1,15 +1,26 @@ /** * Dynamic budget allocation for skill sections. * - * Total SKILL.md body should stay under ~300 lines (≈5,000 words per Agent Skills guide). - * When more sections are enabled, each gets proportionally less space. - * When a package has many releases, API changes budget scales up to capture more churn. + * Total SKILL.md target is ~500 lines. Overhead (frontmatter, header, search, footer) + * is subtracted to get the available body budget, which is divided among enabled sections. + * When a package has many releases, budgets scale up. */ -/** Scale max lines based on enabled section count. Solo sections get full budget, 4 sections ~60%. */ -export function maxLines(min: number, max: number, sectionCount?: number): number { +const TOTAL_TARGET = 500 +const DEFAULT_OVERHEAD = 30 + +/** Available body lines after overhead is subtracted */ +function remainingLines(overheadLines?: number): number { + return TOTAL_TARGET - (overheadLines ?? DEFAULT_OVERHEAD) +} + +/** Scale max lines based on enabled section count and available remaining space. */ +export function maxLines(min: number, max: number, sectionCount?: number, overheadLines?: number): number { + const remaining = remainingLines(overheadLines) + const sections = Math.max(1, sectionCount ?? 1) + const perSection = Math.floor(remaining / sections) const scale = budgetScale(sectionCount) - return Math.max(min, Math.round(max * scale)) + return Math.max(min, Math.min(Math.round(max * scale), perSection)) } /** Scale item count based on enabled section count. */ diff --git a/src/agent/prompts/optional/custom.ts b/src/agent/prompts/optional/custom.ts index ba2c9d7e..2ddb73b2 100644 --- a/src/agent/prompts/optional/custom.ts +++ b/src/agent/prompts/optional/custom.ts @@ -2,8 +2,8 @@ import type { CustomPrompt, PromptSection, SectionValidationWarning } from './ty import { maxLines } from './budget.ts' import { checkAbsolutePaths, checkLineCount, checkSourceCoverage, checkSourcePaths, checkSparseness } from './validate.ts' -export function customSection({ heading, body }: CustomPrompt, enabledSectionCount?: number): PromptSection { - const customMaxLines = maxLines(50, 80, enabledSectionCount) +export function customSection({ heading, body }: CustomPrompt, enabledSectionCount?: number, overheadLines?: number): PromptSection { + const customMaxLines = maxLines(50, 80, enabledSectionCount, overheadLines) return { validate(content: string): SectionValidationWarning[] { diff --git a/src/agent/prompts/optional/types.ts b/src/agent/prompts/optional/types.ts index 2f5040c1..c704a53c 100644 --- a/src/agent/prompts/optional/types.ts +++ b/src/agent/prompts/optional/types.ts @@ -39,6 +39,8 @@ export interface SectionContext { enabledSectionCount?: number /** Number of release files — used for adaptive API changes budget */ releaseCount?: number + /** Lines consumed by frontmatter + header + search + footer */ + overheadLines?: number } export interface CustomPrompt { diff --git a/src/agent/prompts/prompt.ts b/src/agent/prompts/prompt.ts index 4ae07524..731d3445 100644 --- a/src/agent/prompts/prompt.ts +++ b/src/agent/prompts/prompt.ts @@ -68,6 +68,8 @@ export interface BuildSkillPromptOptions { enabledSectionCount?: number /** Key files from the package (e.g., dist/pkg.d.ts) — surfaced in prompt for tool hints */ pkgFiles?: string[] + /** Lines consumed by SKILL.md overhead (frontmatter + header + search + footer) */ + overheadLines?: number } /** @@ -171,7 +173,7 @@ function getSectionDef(section: SkillSection, ctx: SectionContext, customPrompt? switch (section) { case 'api-changes': return apiChangesSection(ctx) case 'best-practices': return bestPracticesSection(ctx) - case 'custom': return customPrompt ? customSection(customPrompt, ctx.enabledSectionCount) : null + case 'custom': return customPrompt ? customSection(customPrompt, ctx.enabledSectionCount, ctx.overheadLines) : null } } @@ -204,7 +206,7 @@ export function buildSectionPrompt(opts: BuildSkillPromptOptions & { section: Sk const m = f.match(/v\d+\.(\d+)\.(\d+)\.md$/) return m && (m[1] === '0' || m[2] === '0') // major (x.0.y) or minor (x.y.0) }).length - const ctx: SectionContext = { packageName, version, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles: opts.pkgFiles, features: opts.features, enabledSectionCount: opts.enabledSectionCount, releaseCount } + const ctx: SectionContext = { packageName, version, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, pkgFiles: opts.pkgFiles, features: opts.features, enabledSectionCount: opts.enabledSectionCount, releaseCount, overheadLines: opts.overheadLines } const sectionDef = getSectionDef(section, ctx, customPrompt) if (!sectionDef) return '' diff --git a/src/agent/prompts/skill.ts b/src/agent/prompts/skill.ts index ba681d57..b50312b2 100644 --- a/src/agent/prompts/skill.ts +++ b/src/agent/prompts/skill.ts @@ -13,8 +13,6 @@ export interface SkillOptions { name: string version?: string releasedAt?: string - /** Production dependencies with version specifiers */ - dependencies?: Record /** npm dist-tags with version and release date */ distTags?: Record globs?: string[] @@ -70,7 +68,7 @@ function formatShortDate(isoDate: string): string { return `${months[date.getUTCMonth()]} ${date.getUTCFullYear()}` } -function generatePackageHeader({ name, description, version, releasedAt, dependencies, distTags, repoUrl, hasIssues, hasDiscussions, hasReleases, docsType, pkgFiles, packages, eject }: SkillOptions): string { +function generatePackageHeader({ name, description, version, releasedAt, distTags, repoUrl, hasIssues, hasDiscussions, hasReleases, docsType, pkgFiles, packages, eject }: SkillOptions): string { let title = `# ${name}` if (repoUrl) { const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}` @@ -89,15 +87,10 @@ function generatePackageHeader({ name, description, version, releasedAt, depende lines.push('', `**Version:** ${versionStr}`) } - if (dependencies && Object.keys(dependencies).length > 0) { - const deps = Object.entries(dependencies) - .map(([n, v]) => `${n}@${v}`) - .join(', ') - lines.push(`**Deps:** ${deps}`) - } - if (distTags && Object.keys(distTags).length > 0) { const tags = Object.entries(distTags) + .sort(([, a], [, b]) => (b.releasedAt ?? '').localeCompare(a.releasedAt ?? '')) + .slice(0, 3) .map(([tag, info]) => { const relDate = info.releasedAt ? ` (${formatShortDate(info.releasedAt)})` : '' return `${tag}: ${info.version}${relDate}` diff --git a/src/commands/install.ts b/src/commands/install.ts index bf5c36f7..cf1ebf0d 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -576,13 +576,11 @@ async function enhanceRegenerated( const cwd = process.cwd() const pkgPath = resolvePkgDir(pkgName, cwd, version) let description: string | undefined - let dependencies: Record | undefined if (pkgPath) { const pkgJsonPath = join(pkgPath, 'package.json') if (existsSync(pkgJsonPath)) { const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) description = pkg.description - dependencies = pkg.dependencies } } @@ -600,7 +598,6 @@ async function enhanceRegenerated( name: pkgName, version, description, - dependencies, body: optimized, relatedSkills: [], hasIssues, @@ -659,13 +656,11 @@ function regenerateBaseSkillMd( // Read description + deps from local package.json const pkgPath = resolvePkgDir(pkgName, cwd, version) let description: string | undefined - let dependencies: Record | undefined if (pkgPath) { const pkgJsonPath = join(pkgPath, 'package.json') if (existsSync(pkgJsonPath)) { const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) description = pkg.description - dependencies = pkg.dependencies } } @@ -696,7 +691,6 @@ function regenerateBaseSkillMd( name: pkgName, version, description, - dependencies, relatedSkills, hasIssues, hasDiscussions, diff --git a/src/commands/sync-parallel.ts b/src/commands/sync-parallel.ts index 5d232e0f..065d9b7a 100644 --- a/src/commands/sync-parallel.ts +++ b/src/commands/sync-parallel.ts @@ -130,6 +130,8 @@ interface BaseSkillData { /** Whether the existing SKILL.md had LLM-generated content */ wasEnhanced?: boolean usedCache: boolean + /** Lines consumed by SKILL.md overhead */ + overheadLines?: number } export async function syncPackagesParallel(config: ParallelSyncConfig): Promise { @@ -260,7 +262,7 @@ export async function syncPackagesParallel(config: ParallelSyncConfig): Promise< name: resolvedName, version: data.version, releasedAt: data.resolved.releasedAt, - dependencies: data.resolved.dependencies, + distTags: data.resolved.distTags, body: cachedBody, relatedSkills: data.relatedSkills, @@ -350,6 +352,7 @@ export async function syncPackagesParallel(config: ParallelSyncConfig): Promise< sections: llmConfig.sections, customPrompt: llmConfig.customPrompt, features: data.features, + overheadLines: data.overheadLines, }) } } @@ -567,7 +570,7 @@ async function syncBaseSkill( version, releasedAt: resolved.releasedAt, description: resolved.description, - dependencies: resolved.dependencies, + distTags: resolved.distTags, relatedSkills, hasIssues: resources.hasIssues, @@ -583,6 +586,7 @@ async function syncBaseSkill( features, }) writeFileSync(join(skillDir, 'SKILL.md'), skillMd) + const overheadLines = skillMd.split('\n').length // Link shared dir to per-agent dirs const shared = !config.global && getSharedSkillsDir(cwd) @@ -614,6 +618,7 @@ async function syncBaseSkill( oldVersion: preLock?.version, oldSyncedAt: preLock?.syncedAt, wasEnhanced: preEnhanced, + overheadLines, } } @@ -652,6 +657,7 @@ async function enhanceWithLLM( customPrompt, features: data.features, pkgFiles: data.pkgFiles, + overheadLines: data.overheadLines, onProgress: (progress) => { const isReasoning = progress.type === 'reasoning' const status = isReasoning ? 'exploring' : 'generating' @@ -671,7 +677,6 @@ async function enhanceWithLLM( name: packageName, version: data.version, releasedAt: data.resolved.releasedAt, - dependencies: data.resolved.dependencies, distTags: data.resolved.distTags, body: optimized, relatedSkills: data.relatedSkills, diff --git a/src/commands/sync-shared.ts b/src/commands/sync-shared.ts index efce950e..87830d34 100644 --- a/src/commands/sync-shared.ts +++ b/src/commands/sync-shared.ts @@ -1376,10 +1376,11 @@ export interface EnhanceOptions { packages?: Array<{ name: string }> features?: FeaturesConfig eject?: boolean + overheadLines?: number } export async function enhanceSkillWithLLM(opts: EnhanceOptions): Promise { - const { packageName, version, skillDir, dirName, model, resolved, relatedSkills, hasIssues, hasDiscussions, hasReleases, hasChangelog, docsType, hasShippedDocs: shippedDocs, pkgFiles, force, debug, sections, customPrompt, packages, features, eject } = opts + const { packageName, version, skillDir, dirName, model, resolved, relatedSkills, hasIssues, hasDiscussions, hasReleases, hasChangelog, docsType, hasShippedDocs: shippedDocs, pkgFiles, force, debug, sections, customPrompt, packages, features, eject, overheadLines } = opts const effectiveFeatures = features @@ -1403,6 +1404,7 @@ export async function enhanceSkillWithLLM(opts: EnhanceOptions): Promise { customPrompt, features: effectiveFeatures, pkgFiles, + overheadLines, onProgress: createToolProgress(llmLog), }) @@ -1428,7 +1430,7 @@ export async function enhanceSkillWithLLM(opts: EnhanceOptions): Promise { name: packageName, version, releasedAt: resolved.releasedAt, - dependencies: resolved.dependencies, + distTags: resolved.distTags, body: optimized, relatedSkills, @@ -1467,6 +1469,7 @@ export interface WritePromptFilesOptions { sections: SkillSection[] customPrompt?: CustomPrompt features?: FeaturesConfig + overheadLines?: number } /** @@ -1490,6 +1493,7 @@ export function writePromptFiles(opts: WritePromptFilesOptions): SkillSection[] pkgFiles: opts.pkgFiles, customPrompt, features, + overheadLines: opts.overheadLines, sections, }) diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 5c0c41c1..7beeaba4 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -558,7 +558,7 @@ async function syncSinglePackage(packageSpec: string, config: SyncConfig): Promi version, releasedAt: resolved.releasedAt, description: resolved.description, - dependencies: resolved.dependencies, + distTags: resolved.distTags, relatedSkills, hasIssues: resources.hasIssues, @@ -575,6 +575,7 @@ async function syncSinglePackage(packageSpec: string, config: SyncConfig): Promi eject: isEject, }) writeFileSync(join(skillDir, 'SKILL.md'), baseSkillMd) + const overheadLines = baseSkillMd.split('\n').length p.log.success(config.mode === 'update' ? `Updated skill: ${relative(cwd, skillDir)}` : `Created base skill: ${relative(cwd, skillDir)}`) @@ -602,7 +603,7 @@ async function syncSinglePackage(packageSpec: string, config: SyncConfig): Promi version, releasedAt: resolved.releasedAt, description: resolved.description, - dependencies: resolved.dependencies, + distTags: resolved.distTags, body: cachedBody, relatedSkills, @@ -652,6 +653,7 @@ async function syncSinglePackage(packageSpec: string, config: SyncConfig): Promi sections: llmConfig.sections, customPrompt: llmConfig.customPrompt, features, + overheadLines, }) } else if (llmConfig) { @@ -678,6 +680,7 @@ async function syncSinglePackage(packageSpec: string, config: SyncConfig): Promi packages: allPackages.length > 1 ? allPackages : undefined, features, eject: isEject, + overheadLines, }) } } @@ -1083,7 +1086,7 @@ export async function exportPortablePrompts(packageSpec: string, opts: { version, releasedAt: resolved.releasedAt, description: resolved.description, - dependencies: resolved.dependencies, + distTags: resolved.distTags, relatedSkills, hasIssues: resources.hasIssues,