From fd9b2a11d0b2da64fb9ebbce8125da48351a5229 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 15 Mar 2026 20:05:41 -0500 Subject: [PATCH] docs+test: SKILL authoring guidance + regression tests Adds "Writing SKILL templates" section to CLAUDE.md explaining that templates are prompts, not scripts. Adds validation test catching hardcoded 'main' in git commands, and resolver content test. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 17 ++++++++++ test/gen-skill-docs.test.ts | 21 +++++++++++++ test/skill-validation.test.ts | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index c6909357..31a49a98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,23 @@ SKILL.md files are **generated** from `.tmpl` templates. To update docs: To add a new browse command: add it to `browse/src/commands.ts` and rebuild. To add a snapshot flag: add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts` and rebuild. +## Writing SKILL templates + +SKILL.md.tmpl files are **prompt templates read by Claude**, not bash scripts. +Each bash code block runs in a separate shell — variables do not persist between blocks. + +Rules: +- **Use natural language for logic and state.** Don't use shell variables to pass + state between code blocks. Instead, tell Claude what to remember and reference + it in prose (e.g., "the base branch detected in Step 0"). +- **Don't hardcode branch names.** Detect `main`/`master`/etc dynamically via + `gh pr view` or `gh repo view`. Use `{{BASE_BRANCH_DETECT}}` for PR-targeting + skills. Use "the base branch" in prose, `` in code block placeholders. +- **Keep bash blocks self-contained.** Each code block should work independently. + If a block needs context from a previous step, restate it in the prose above. +- **Express conditionals as English.** Instead of nested `if/elif/else` in bash, + write numbered decision steps: "1. If X, do Y. 2. Otherwise, do Z." + ## Browser interaction When you need to interact with a browser (QA, dogfooding, cookie setup), use the diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 264cb904..a8347fed 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -131,6 +131,27 @@ describe('gen-skill-docs', () => { }); }); +describe('BASE_BRANCH_DETECT resolver', () => { + // Find a generated SKILL.md that uses the placeholder (ship is guaranteed to) + const shipContent = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8'); + + test('resolver output contains PR base detection command', () => { + expect(shipContent).toContain('gh pr view --json baseRefName'); + }); + + test('resolver output contains repo default branch detection command', () => { + expect(shipContent).toContain('gh repo view --json defaultBranchRef'); + }); + + test('resolver output contains fallback to main', () => { + expect(shipContent).toMatch(/fall\s*back\s+to\s+`main`/i); + }); + + test('resolver output uses "the base branch" phrasing', () => { + expect(shipContent).toContain('the base branch'); + }); +}); + /** * Quality evals — catch description regressions. * diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 05a39761..b29471e4 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -361,6 +361,64 @@ describe('Greptile history format consistency', () => { }); }); +// --- Hardcoded branch name detection in templates --- + +describe('No hardcoded branch names in SKILL templates', () => { + const tmplFiles = [ + 'ship/SKILL.md.tmpl', + 'review/SKILL.md.tmpl', + 'qa/SKILL.md.tmpl', + 'plan-ceo-review/SKILL.md.tmpl', + 'retro/SKILL.md.tmpl', + ]; + + // Patterns that indicate hardcoded 'main' in git commands + const gitMainPatterns = [ + /\bgit\s+diff\s+(?:origin\/)?main\b/, + /\bgit\s+log\s+(?:origin\/)?main\b/, + /\bgit\s+fetch\s+origin\s+main\b/, + /\bgit\s+merge\s+origin\/main\b/, + /\borigin\/main\b/, + ]; + + // Lines that are allowed to mention 'main' (fallback logic, prose) + const allowlist = [ + /fall\s*back\s+to\s+`main`/i, + /fall\s*back\s+to\s+`?main`?/i, + /typically\s+`?main`?/i, + /If\s+on\s+`main`/i, // old pattern — should not exist + ]; + + for (const tmplFile of tmplFiles) { + test(`${tmplFile} has no hardcoded 'main' in git commands`, () => { + const filePath = path.join(ROOT, tmplFile); + if (!fs.existsSync(filePath)) return; + const lines = fs.readFileSync(filePath, 'utf-8').split('\n'); + const violations: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const isAllowlisted = allowlist.some(p => p.test(line)); + if (isAllowlisted) continue; + + for (const pattern of gitMainPatterns) { + if (pattern.test(line)) { + violations.push(`Line ${i + 1}: ${line.trim()}`); + break; + } + } + } + + if (violations.length > 0) { + throw new Error( + `${tmplFile} has hardcoded 'main' in git commands:\n` + + violations.map(v => ` ${v}`).join('\n') + ); + } + }); + } +}); + // --- Part 7b: TODOS-format.md reference consistency --- describe('TODOS-format.md reference consistency', () => {