Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions gstack/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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.
Expand All @@ -30,7 +29,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.
Expand Down
13 changes: 11 additions & 2 deletions scripts/gen-llms-txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const OUTPUT = path.join(ROOT, 'gstack', 'llms.txt');
interface SkillEntry {
name: string;
description: string;
href: string;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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));

Expand All @@ -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('');

Expand Down
16 changes: 14 additions & 2 deletions test/llms-txt-shape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down