From 55622ba7da55b8c40c9d439c6454f123bd93ab58 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 24 Mar 2026 14:45:32 +1100 Subject: [PATCH 1/3] refactor: drop dependencies from SKILL.md, limit tags to 3 most recent Remove the Deps line from generated SKILL.md entirely as it duplicates what's already in node_modules. Limit Tags to the 3 most recently released dist-tags, sorted by release date, with dates shown. --- src/agent/prompts/skill.ts | 13 +++---------- src/commands/install.ts | 6 ------ src/commands/sync-parallel.ts | 5 ++--- src/commands/sync-shared.ts | 2 +- src/commands/sync.ts | 6 +++--- 5 files changed, 9 insertions(+), 23 deletions(-) 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..e08e96ae 100644 --- a/src/commands/sync-parallel.ts +++ b/src/commands/sync-parallel.ts @@ -260,7 +260,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, @@ -567,7 +567,7 @@ async function syncBaseSkill( version, releasedAt: resolved.releasedAt, description: resolved.description, - dependencies: resolved.dependencies, + distTags: resolved.distTags, relatedSkills, hasIssues: resources.hasIssues, @@ -671,7 +671,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..7875a2f8 100644 --- a/src/commands/sync-shared.ts +++ b/src/commands/sync-shared.ts @@ -1428,7 +1428,7 @@ export async function enhanceSkillWithLLM(opts: EnhanceOptions): Promise { name: packageName, version, releasedAt: resolved.releasedAt, - dependencies: resolved.dependencies, + distTags: resolved.distTags, body: optimized, relatedSkills, diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 5c0c41c1..b70b0cc1 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, @@ -602,7 +602,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, @@ -1083,7 +1083,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, From 890fcd3c2634756b46f6f85ed4899d26c67ff736 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 24 Mar 2026 15:21:57 +1100 Subject: [PATCH 2/3] feat: overhead-aware section budgets, raise total target to 500 lines Section budgets now account for actual SKILL.md overhead (frontmatter, header, search block, footer). The base SKILL.md line count is passed through to budget functions so sections fill available space rather than using fixed estimates. Total target raised from 300 to 500 lines. Per-section limits bumped proportionally: best-practices 100/250 lines 6/15 items, api-changes 60/130 lines 8/18 items. --- src/agent/clis/index.ts | 3 ++- src/agent/clis/types.ts | 2 ++ src/agent/prompts/optional/api-changes.ts | 6 +++--- src/agent/prompts/optional/best-practices.ts | 6 +++--- src/agent/prompts/optional/budget.ts | 21 ++++++++++++++------ src/agent/prompts/optional/custom.ts | 4 ++-- src/agent/prompts/optional/types.ts | 2 ++ src/agent/prompts/prompt.ts | 6 ++++-- src/commands/sync-parallel.ts | 6 ++++++ src/commands/sync-shared.ts | 6 +++++- src/commands/sync.ts | 3 +++ 11 files changed, 47 insertions(+), 18 deletions(-) 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..4b8ca8b3 100644 --- a/src/agent/prompts/optional/budget.ts +++ b/src/agent/prompts/optional/budget.ts @@ -1,15 +1,24 @@ /** * 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 scale = budgetScale(sectionCount) - return Math.max(min, Math.round(max * scale)) + return Math.max(min, Math.min(Math.round(max * scale), remaining)) } /** 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/commands/sync-parallel.ts b/src/commands/sync-parallel.ts index e08e96ae..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 { @@ -350,6 +352,7 @@ export async function syncPackagesParallel(config: ParallelSyncConfig): Promise< sections: llmConfig.sections, customPrompt: llmConfig.customPrompt, features: data.features, + overheadLines: data.overheadLines, }) } } @@ -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' diff --git a/src/commands/sync-shared.ts b/src/commands/sync-shared.ts index 7875a2f8..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), }) @@ -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 b70b0cc1..7beeaba4 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -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)}`) @@ -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, }) } } From 4e4826b8077d7091814fb689703d6b0b77a5cb99 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 24 Mar 2026 15:36:13 +1100 Subject: [PATCH 3/3] fix: cap section budget to per-section share of remaining lines --- src/agent/prompts/optional/budget.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agent/prompts/optional/budget.ts b/src/agent/prompts/optional/budget.ts index 4b8ca8b3..1387bfc1 100644 --- a/src/agent/prompts/optional/budget.ts +++ b/src/agent/prompts/optional/budget.ts @@ -17,8 +17,10 @@ function remainingLines(overheadLines?: number): number { /** 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.min(Math.round(max * scale), remaining)) + return Math.max(min, Math.min(Math.round(max * scale), perSection)) } /** Scale item count based on enabled section count. */