mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
fix: add codex skill metadata for gstack skills (#339)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-autoplan"
|
||||
short_description: "Auto-review pipeline — reads the full CEO, design, and eng review skills from disk and runs them sequentially with..."
|
||||
default_prompt: "Use gstack-autoplan for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-benchmark"
|
||||
short_description: "Performance regression detection using the browse daemon. Establishes baselines for page load times, Core Web..."
|
||||
default_prompt: "Use gstack-benchmark for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-browse"
|
||||
short_description: "Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with elements, verify page..."
|
||||
default_prompt: "Use gstack-browse for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-canary"
|
||||
short_description: "Post-deploy canary monitoring. Watches the live app for console errors, performance regressions, and page failures..."
|
||||
default_prompt: "Use gstack-canary for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-careful"
|
||||
short_description: "Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE, force-push, git reset --hard, kubectl..."
|
||||
default_prompt: "Use gstack-careful for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-cso"
|
||||
short_description: "Chief Security Officer mode. Performs OWASP Top 10 audit, STRIDE threat modeling, attack surface analysis, auth flow..."
|
||||
default_prompt: "Use gstack-cso for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-design-consultation"
|
||||
short_description: "Design consultation: understands your product, researches the landscape, proposes a complete design system..."
|
||||
default_prompt: "Use gstack-design-consultation for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-design-review"
|
||||
short_description: "Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems, AI slop patterns, and slow..."
|
||||
default_prompt: "Use gstack-design-review for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-document-release"
|
||||
short_description: "Post-ship documentation update. Reads all project docs, cross-references the diff, updates..."
|
||||
default_prompt: "Use gstack-document-release for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-freeze"
|
||||
short_description: "Restrict file edits to a specific directory for the session. Blocks Edit and Write outside the allowed path. Use..."
|
||||
default_prompt: "Use gstack-freeze for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-guard"
|
||||
short_description: "Full safety mode: destructive command warnings + directory-scoped edits. Combines /careful (warns before rm -rf,..."
|
||||
default_prompt: "Use gstack-guard for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-investigate"
|
||||
short_description: "Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron..."
|
||||
default_prompt: "Use gstack-investigate for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-land-and-deploy"
|
||||
short_description: "Land and deploy workflow. Merges the PR, waits for CI and deploy, verifies production health via canary checks...."
|
||||
default_prompt: "Use gstack-land-and-deploy for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-office-hours"
|
||||
short_description: "YC Office Hours — two modes. Startup mode: six forcing questions that expose demand reality, status quo, desperate..."
|
||||
default_prompt: "Use gstack-office-hours for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-plan-ceo-review"
|
||||
short_description: "CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when..."
|
||||
default_prompt: "Use gstack-plan-ceo-review for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-plan-design-review"
|
||||
short_description: "Designer's eye plan review — interactive, like CEO and Eng review. Rates each design dimension 0-10, explains what..."
|
||||
default_prompt: "Use gstack-plan-design-review for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-plan-eng-review"
|
||||
short_description: "Eng manager-mode plan review. Lock in the execution plan — architecture, data flow, diagrams, edge cases, test..."
|
||||
default_prompt: "Use gstack-plan-eng-review for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-qa-only"
|
||||
short_description: "Report-only QA testing. Systematically tests a web application and produces a structured report with health score,..."
|
||||
default_prompt: "Use gstack-qa-only for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-qa"
|
||||
short_description: "Systematically QA test a web application and fix bugs found. Runs QA testing, then iteratively fixes bugs in source..."
|
||||
default_prompt: "Use gstack-qa for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-retro"
|
||||
short_description: "Weekly engineering retrospective. Analyzes commit history, work patterns, and code quality metrics with persistent..."
|
||||
default_prompt: "Use gstack-retro for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-review"
|
||||
short_description: "Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust boundary violations,..."
|
||||
default_prompt: "Use gstack-review for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-setup-browser-cookies"
|
||||
short_description: "Import cookies from your real browser (Comet, Chrome, Arc, Brave, Edge) into the headless browse session. Opens an..."
|
||||
default_prompt: "Use gstack-setup-browser-cookies for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-setup-deploy"
|
||||
short_description: "Configure deployment settings for /land-and-deploy. Detects your deploy platform (Fly.io, Render, Vercel, Netlify,..."
|
||||
default_prompt: "Use gstack-setup-deploy for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-ship"
|
||||
short_description: "Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push,..."
|
||||
default_prompt: "Use gstack-ship for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-unfreeze"
|
||||
short_description: "Clear the freeze boundary set by /freeze, allowing edits to all directories again. Use when you want to widen edit..."
|
||||
default_prompt: "Use gstack-unfreeze for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack-upgrade"
|
||||
short_description: "Upgrade gstack to the latest version. Detects global vs vendored install, runs the upgrade, and shows what's new...."
|
||||
default_prompt: "Use gstack-upgrade for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "gstack"
|
||||
short_description: "Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with elements, verify page..."
|
||||
default_prompt: "Use gstack for this task."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "gstack"
|
||||
short_description: "Bundle of gstack Codex skills"
|
||||
default_prompt: "Use $gstack to locate the bundled gstack skills."
|
||||
+75
-41
@@ -20,6 +20,7 @@ const DRY_RUN = process.argv.includes('--dry-run');
|
||||
// ─── Template Context ───────────────────────────────────────
|
||||
|
||||
type Host = 'claude' | 'codex';
|
||||
const OPENAI_SHORT_DESCRIPTION_LIMIT = 120;
|
||||
|
||||
const HOST_ARG = process.argv.find(a => a.startsWith('--host'));
|
||||
const HOST: Host = (() => {
|
||||
@@ -2834,6 +2835,65 @@ function codexSkillName(skillDir: string): string {
|
||||
return `gstack-${skillDir}`;
|
||||
}
|
||||
|
||||
function extractNameAndDescription(content: string): { name: string; description: string } {
|
||||
const fmStart = content.indexOf('---\n');
|
||||
if (fmStart !== 0) return { name: '', description: '' };
|
||||
const fmEnd = content.indexOf('\n---', fmStart + 4);
|
||||
if (fmEnd === -1) return { name: '', description: '' };
|
||||
|
||||
const frontmatter = content.slice(fmStart + 4, fmEnd);
|
||||
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
||||
const name = nameMatch ? nameMatch[1].trim() : '';
|
||||
|
||||
let description = '';
|
||||
const lines = frontmatter.split('\n');
|
||||
let inDescription = false;
|
||||
const descLines: string[] = [];
|
||||
for (const line of lines) {
|
||||
if (line.match(/^description:\s*\|?\s*$/)) {
|
||||
inDescription = true;
|
||||
continue;
|
||||
}
|
||||
if (line.match(/^description:\s*\S/)) {
|
||||
description = line.replace(/^description:\s*/, '').trim();
|
||||
break;
|
||||
}
|
||||
if (inDescription) {
|
||||
if (line === '' || line.match(/^\s/)) {
|
||||
descLines.push(line.replace(/^ /, ''));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (descLines.length > 0) {
|
||||
description = descLines.join('\n').trim();
|
||||
}
|
||||
|
||||
return { name, description };
|
||||
}
|
||||
|
||||
function condenseOpenAIShortDescription(description: string): string {
|
||||
const firstParagraph = description.split(/\n\s*\n/)[0] || description;
|
||||
const collapsed = firstParagraph.replace(/\s+/g, ' ').trim();
|
||||
if (collapsed.length <= OPENAI_SHORT_DESCRIPTION_LIMIT) return collapsed;
|
||||
|
||||
const truncated = collapsed.slice(0, OPENAI_SHORT_DESCRIPTION_LIMIT - 3);
|
||||
const lastSpace = truncated.lastIndexOf(' ');
|
||||
const safe = lastSpace > 40 ? truncated.slice(0, lastSpace) : truncated;
|
||||
return `${safe}...`;
|
||||
}
|
||||
|
||||
function generateOpenAIYaml(displayName: string, shortDescription: string): string {
|
||||
return `interface:
|
||||
display_name: ${JSON.stringify(displayName)}
|
||||
short_description: ${JSON.stringify(shortDescription)}
|
||||
default_prompt: ${JSON.stringify(`Use ${displayName} for this task.`)}
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform frontmatter for Codex: keep only name + description.
|
||||
* Strips allowed-tools, hooks, version, and all other fields.
|
||||
@@ -2842,48 +2902,12 @@ function codexSkillName(skillDir: string): string {
|
||||
function transformFrontmatter(content: string, host: Host): string {
|
||||
if (host === 'claude') return content;
|
||||
|
||||
// Find frontmatter boundaries
|
||||
const fmStart = content.indexOf('---\n');
|
||||
if (fmStart !== 0) return content; // frontmatter must be at the start
|
||||
if (fmStart !== 0) return content;
|
||||
const fmEnd = content.indexOf('\n---', fmStart + 4);
|
||||
if (fmEnd === -1) return content;
|
||||
|
||||
const frontmatter = content.slice(fmStart + 4, fmEnd);
|
||||
const body = content.slice(fmEnd + 4); // includes the leading \n after ---
|
||||
|
||||
// Parse name
|
||||
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
||||
const name = nameMatch ? nameMatch[1].trim() : '';
|
||||
|
||||
// Parse description — handle both simple and block scalar (|) formats
|
||||
let description = '';
|
||||
const lines = frontmatter.split('\n');
|
||||
let inDescription = false;
|
||||
const descLines: string[] = [];
|
||||
for (const line of lines) {
|
||||
if (line.match(/^description:\s*\|?\s*$/)) {
|
||||
// Block scalar start: "description: |" or "description:"
|
||||
inDescription = true;
|
||||
continue;
|
||||
}
|
||||
if (line.match(/^description:\s*\S/)) {
|
||||
// Simple inline: "description: some text"
|
||||
description = line.replace(/^description:\s*/, '').trim();
|
||||
break;
|
||||
}
|
||||
if (inDescription) {
|
||||
// Block scalar continuation — indented lines (2 spaces) or blank lines
|
||||
if (line === '' || line.match(/^\s/)) {
|
||||
descLines.push(line.replace(/^ /, ''));
|
||||
} else {
|
||||
// End of block scalar — hit a non-indented, non-blank line
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (descLines.length > 0) {
|
||||
description = descLines.join('\n').trim();
|
||||
}
|
||||
const { name, description } = extractNameAndDescription(content);
|
||||
|
||||
// Re-emit Codex frontmatter (name + description only)
|
||||
const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n');
|
||||
@@ -2930,6 +2954,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
const tmplContent = fs.readFileSync(tmplPath, 'utf-8');
|
||||
const relTmplPath = path.relative(ROOT, tmplPath);
|
||||
let outputPath = tmplPath.replace(/\.tmpl$/, '');
|
||||
let outputDir: string | null = null;
|
||||
|
||||
// Determine skill directory relative to ROOT
|
||||
const skillDir = path.relative(ROOT, path.dirname(tmplPath));
|
||||
@@ -2937,14 +2962,14 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
// For codex host, route output to .agents/skills/{codexSkillName}/SKILL.md
|
||||
if (host === 'codex') {
|
||||
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
|
||||
const outputDir = path.join(ROOT, '.agents', 'skills', codexName);
|
||||
outputDir = path.join(ROOT, '.agents', 'skills', codexName);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
outputPath = path.join(outputDir, 'SKILL.md');
|
||||
}
|
||||
|
||||
// Extract skill name from frontmatter for TemplateContext
|
||||
const nameMatch = tmplContent.match(/^name:\s*(.+)$/m);
|
||||
const skillName = nameMatch ? nameMatch[1].trim() : path.basename(path.dirname(tmplPath));
|
||||
const { name: extractedName, description: extractedDescription } = extractNameAndDescription(tmplContent);
|
||||
const skillName = extractedName || path.basename(path.dirname(tmplPath));
|
||||
|
||||
// Extract benefits-from list from frontmatter (inline YAML: benefits-from: [a, b])
|
||||
const benefitsMatch = tmplContent.match(/^benefits-from:\s*\[([^\]]*)\]/m);
|
||||
@@ -2986,6 +3011,15 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
|
||||
content = content.replace(/\.claude\/skills\/gstack/g, ctx.paths.localSkillRoot);
|
||||
content = content.replace(/\.claude\/skills\/review/g, '.agents/skills/gstack/review');
|
||||
content = content.replace(/\.claude\/skills/g, '.agents/skills');
|
||||
|
||||
if (outputDir) {
|
||||
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
|
||||
const agentsDir = path.join(outputDir, 'agents');
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
const displayName = codexName;
|
||||
const shortDescription = condenseOpenAIShortDescription(extractedDescription);
|
||||
fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), generateOpenAIYaml(displayName, shortDescription));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend generated header (after frontmatter)
|
||||
|
||||
@@ -942,6 +942,14 @@ describe('Codex generation (--host codex)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('root gstack bundle has OpenAI metadata for Codex skill browsing', () => {
|
||||
const rootMetadata = path.join(ROOT, 'agents', 'openai.yaml');
|
||||
expect(fs.existsSync(rootMetadata)).toBe(true);
|
||||
const content = fs.readFileSync(rootMetadata, 'utf-8');
|
||||
expect(content).toContain('display_name: "gstack"');
|
||||
expect(content).toContain('Use $gstack to locate the bundled gstack skills.');
|
||||
});
|
||||
|
||||
test('codexSkillName mapping: root is gstack, others are gstack-{dir}', () => {
|
||||
// Root → gstack
|
||||
expect(fs.existsSync(path.join(AGENTS_DIR, 'gstack', 'SKILL.md'))).toBe(true);
|
||||
@@ -971,6 +979,17 @@ describe('Codex generation (--host codex)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('all Codex skills have agents/openai.yaml metadata', () => {
|
||||
for (const skill of CODEX_SKILLS) {
|
||||
const metadata = path.join(AGENTS_DIR, skill.codexName, 'agents', 'openai.yaml');
|
||||
expect(fs.existsSync(metadata)).toBe(true);
|
||||
const content = fs.readFileSync(metadata, 'utf-8');
|
||||
expect(content).toContain(`display_name: "${skill.codexName}"`);
|
||||
expect(content).toContain('short_description:');
|
||||
expect(content).toContain('allow_implicit_invocation: true');
|
||||
}
|
||||
});
|
||||
|
||||
test('no .claude/skills/ in Codex output', () => {
|
||||
for (const skill of CODEX_SKILLS) {
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, skill.codexName, 'SKILL.md'), 'utf-8');
|
||||
|
||||
@@ -98,7 +98,8 @@ export function parseCodexJSONL(lines: string[]): ParsedCodexJSONL {
|
||||
|
||||
/**
|
||||
* Install a SKILL.md into a temp HOME directory for Codex to discover.
|
||||
* Creates ~/.codex/skills/{skillName}/SKILL.md in the temp HOME.
|
||||
* Creates ~/.codex/skills/{skillName}/SKILL.md in the temp HOME and copies
|
||||
* agents/openai.yaml when present so Codex sees the same metadata as a real install.
|
||||
*
|
||||
* Returns the temp HOME path. Caller is responsible for cleanup.
|
||||
*/
|
||||
@@ -116,6 +117,13 @@ export function installSkillToTempHome(
|
||||
fs.copyFileSync(srcSkill, path.join(destDir, 'SKILL.md'));
|
||||
}
|
||||
|
||||
const srcOpenAIYaml = path.join(skillDir, 'agents', 'openai.yaml');
|
||||
if (fs.existsSync(srcOpenAIYaml)) {
|
||||
const destAgentsDir = path.join(destDir, 'agents');
|
||||
fs.mkdirSync(destAgentsDir, { recursive: true });
|
||||
fs.copyFileSync(srcOpenAIYaml, path.join(destAgentsDir, 'openai.yaml'));
|
||||
}
|
||||
|
||||
return home;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user