fix: preserve generated skill links

This commit is contained in:
Nader Helmy 2026-07-15 19:05:10 -05:00
parent 6148354529
commit d6e6c90aae
No known key found for this signature in database
3 changed files with 26 additions and 6 deletions

View File

@ -15,7 +15,6 @@ Conventions:
- [/browse](browse/SKILL.md): Fast headless browser for QA testing and site dogfooding.
- [/canary](canary/SKILL.md): Post-deploy canary monitoring.
- [/careful](careful/SKILL.md): Safety guardrails for destructive commands.
- [/claude](claude/SKILL.md): Claude Code CLI wrapper for non-Claude hosts - three modes.
- [/codex](codex/SKILL.md): OpenAI Codex CLI wrapper — three modes.
- [/context-restore](context-restore/SKILL.md): Restore working context saved earlier by /context-save.
- [/context-save](context-save/SKILL.md): Save working context.
@ -29,7 +28,7 @@ Conventions:
- [/document-generate](document-generate/SKILL.md): Generate missing documentation from scratch for a feature, module, or entire project.
- [/document-release](document-release/SKILL.md): Post-ship documentation update.
- [/freeze](freeze/SKILL.md): Restrict file edits to a specific directory for the session.
- [/gstack](gstack/SKILL.md): Router for the gstack skill suite.
- [/gstack](SKILL.md): Router for the gstack skill suite.
- [/gstack-upgrade](gstack-upgrade/SKILL.md): Upgrade gstack to the latest version.
- [/guard](guard/SKILL.md): Full safety mode: destructive command warnings + directory-scoped edits.
- [/health](health/SKILL.md): Code quality dashboard.

View File

@ -28,6 +28,7 @@ const OUTPUT = path.join(ROOT, 'gstack', 'llms.txt');
interface SkillEntry {
name: string;
description: string;
href: string;
}
/**
@ -113,6 +114,11 @@ function oneLine(text: string): string {
return first.replace(/\s+/g, ' ').trim();
}
function skillDirForOutput(output: string): string {
const dir = path.dirname(output);
return dir === '.' ? '.' : dir;
}
interface GenerateOptions {
/** Override repo root (for tests). */
root?: string;
@ -135,6 +141,9 @@ export async function generateLlmsTxt(opts: GenerateOptions = {}): Promise<Gener
const templates = discoverTemplates(root);
const skills: SkillEntry[] = [];
for (const t of templates) {
if (skillDirForOutput(t.output) === 'claude') {
continue;
}
const filePath = path.join(root, t.tmpl);
const entry = parseSkillFrontmatter(filePath);
if (!entry) {
@ -144,7 +153,7 @@ export async function generateLlmsTxt(opts: GenerateOptions = {}): Promise<Gener
}
continue;
}
skills.push(entry);
skills.push({ ...entry, href: t.output });
}
skills.sort((a, b) => a.name.localeCompare(b.name));
@ -167,7 +176,7 @@ export async function generateLlmsTxt(opts: GenerateOptions = {}): Promise<Gener
lines.push('');
for (const skill of skills) {
const summary = oneLine(skill.description);
lines.push(`- [/${skill.name}](${skill.name}/SKILL.md): ${summary}`);
lines.push(`- [/${skill.name}](${skill.href}): ${summary}`);
}
lines.push('');

View File

@ -25,17 +25,29 @@ describe('gen-llms-txt — shape', () => {
expect(generated.content).toContain('auto-generated');
});
test('every skill .tmpl in the repo appears in the index', () => {
test('every generated Claude skill appears in the index', () => {
const templates = discoverTemplates(ROOT);
const claudeGeneratedTemplates = templates.filter((t) => path.dirname(t.output) !== 'claude');
// Filter to those that successfully parsed (have name + description).
expect(generated.skills.length).toBeGreaterThan(0);
expect(generated.skills.length).toBeLessThanOrEqual(templates.length);
expect(generated.skills.length).toBeLessThanOrEqual(claudeGeneratedTemplates.length);
expect(generated.content).not.toContain('[/claude]');
for (const skill of generated.skills) {
expect(generated.content).toMatch(new RegExp(`/${skill.name}\\b`));
}
});
test('every skill link points at an existing generated file', () => {
const skillsSection = generated.content.split('## Skills')[1].split('## Browse Commands')[0];
const links = [...skillsSection.matchAll(/- \[\/[^\]]+\]\(([^)]+)\):/g)].map((m) => m[1]);
expect(links.length).toBe(generated.skills.length);
for (const href of links) {
expect(fs.existsSync(path.join(ROOT, href)), href).toBe(true);
}
});
test('every browse command in COMMAND_DESCRIPTIONS appears in the index', () => {
expect(generated.browseCommands.length).toBeGreaterThan(0);
for (const cmd of generated.browseCommands) {