Commit 93f3671a292e

Vincent Demeester <vincent@sbr.pm>
2026-02-06 18:16:06
feat(pi): add dynamic repository-specific review rules from Claude skills
Enhanced review extension to dynamically load repository-specific guidelines from Claude skills with flexible organization patterns. Organization Patterns: 1. **Centralized Review Skill** (recommended): ~/.config/claude/skills/Review/ ├── repositories/ │ ├── tektoncd-pipeline.md # tektoncd/pipeline │ ├── nixos-nixpkgs.md # NixOS/nixpkgs │ └── kubernetes-kubernetes.md # kubernetes/kubernetes ├── workflows/Review.md # General review workflow └── SKILL.md # General review guidelines 2. **Legacy Repo-Specific Skills** (backward compatible): ~/.config/claude/skills/Nixpkgs/ ├── review-checklist.md ├── workflows/Review.md └── SKILL.md Loading Priority: 1. Review/repositories/<org>-<repo>.md (most specific) 2. Review/workflows/Review.md (general workflow) 3. Review/SKILL.md (general guidelines) 4. <Repo>/review-checklist.md (legacy, repo-specific) 5. <Repo>/workflows/Review.md (legacy, repo-specific) 6. <Repo>/SKILL.md (legacy, extracts review section) Dynamic Resolution: - Extracts org/repo from git remote URL - Case-insensitive matching - Tries centralized Review skill first - Falls back to legacy repo-specific skills - Returns null if no review guidelines found Example Remote Mappings: - git@github.com:tektoncd/pipeline.git → tektoncd-pipeline.md - https://github.com/NixOS/nixpkgs.git → nixos-nixpkgs.md - https://github.com/kubernetes/kubernetes → kubernetes-kubernetes.md Benefits: - **Centralized**: All review rules in one skill (Review/) - **Flexible**: Supports both centralized and legacy patterns - **Zero maintenance**: Add repo file, no code changes - **Backward compatible**: Existing skills still work - **Extensible**: Easy to add new repositories - **Clear separation**: Review skill for reviews, repo skills for contribution Note: Having a skill (e.g., Tekton) doesn't mean it has review guidelines. The Tekton skill is for *using* Tekton, not *reviewing* tektoncd repos. Create Review/repositories/tektoncd-pipeline.md for PR review guidelines. See /tmp/review-skill-proposal.md for detailed organization guide.
1 parent edd6160
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/review.ts
@@ -48,6 +48,163 @@ function formatTimestamp(date: Date): string {
 	return date.toISOString().replace("T", " ").slice(0, 19);
 }
 
+function extractOrgRepo(remoteUrl: string): { org: string; repo: string } | null {
+	// Extract org and repo from various git remote formats
+	// Examples:
+	//   https://github.com/NixOS/nixpkgs.git -> { org: "nixos", repo: "nixpkgs" }
+	//   git@github.com:tektoncd/pipeline.git -> { org: "tektoncd", repo: "pipeline" }
+	//   https://github.com/kubernetes/kubernetes -> { org: "kubernetes", repo: "kubernetes" }
+	
+	// Remove .git suffix
+	const cleanUrl = remoteUrl.replace(/\.git$/, "");
+	
+	// Extract org/repo pattern
+	const match = cleanUrl.match(/[:/]([^/]+)\/([^/]+)$/);
+	if (match) {
+		// Lowercase for case-insensitive matching
+		const org = match[1].toLowerCase();
+		const repo = match[2].toLowerCase();
+		return { org, repo };
+	}
+	
+	return null;
+}
+
+async function loadRepositorySpecificRules(cwd: string): Promise<string | null> {
+	try {
+		// Get git remote URL
+		const remoteUrl = execSync("git remote get-url origin", {
+			cwd,
+			encoding: "utf-8",
+		}).trim();
+
+		const orgRepo = extractOrgRepo(remoteUrl);
+		if (!orgRepo) {
+			return null;
+		}
+
+		const { org, repo } = orgRepo;
+		const home = process.env.HOME || process.env.USERPROFILE || "";
+		const skillsDir = path.join(home, ".config", "claude", "skills");
+
+		// Priority 1: Review skill with repo-specific file
+		// ~/.config/claude/skills/Review/repositories/<org>-<repo>.md
+		const reviewSkillDir = path.join(skillsDir, "Review");
+		const repoSpecificFile = path.join(reviewSkillDir, "repositories", `${org}-${repo}.md`);
+		
+		try {
+			const content = await fs.readFile(repoSpecificFile, "utf-8");
+			const filename = path.relative(skillsDir, repoSpecificFile);
+			return `
+
+# Repository-Specific Review Guidelines: ${org}/${repo}
+
+From: ~/.config/claude/skills/${filename}
+
+${content}
+`;
+		} catch {
+			// File doesn't exist, continue to next priority
+		}
+
+		// Priority 2: Review skill general workflow
+		// ~/.config/claude/skills/Review/workflows/Review.md
+		const reviewWorkflow = path.join(reviewSkillDir, "workflows", "Review.md");
+		try {
+			const content = await fs.readFile(reviewWorkflow, "utf-8");
+			const filename = path.relative(skillsDir, reviewWorkflow);
+			return `
+
+# General Review Guidelines
+
+From: ~/.config/claude/skills/${filename}
+
+${content}
+`;
+		} catch {
+			// File doesn't exist, continue to next priority
+		}
+
+		// Priority 3: Review skill general guidelines
+		// ~/.config/claude/skills/Review/SKILL.md
+		const reviewSkill = path.join(reviewSkillDir, "SKILL.md");
+		try {
+			const content = await fs.readFile(reviewSkill, "utf-8");
+			const filename = path.relative(skillsDir, reviewSkill);
+			return `
+
+# General Review Guidelines
+
+From: ~/.config/claude/skills/${filename}
+
+${content}
+`;
+		} catch {
+			// File doesn't exist, continue to fallback
+		}
+
+		// Fallback: Legacy repo-specific skill (e.g., Nixpkgs skill)
+		// Try to find a skill directory matching the repository name
+		const skillDirs = await fs.readdir(skillsDir);
+		const matchingSkill = skillDirs.find(
+			(dir) => dir.toLowerCase() === repo
+		);
+
+		if (!matchingSkill) {
+			return null;
+		}
+
+		const skillDir = path.join(skillsDir, matchingSkill);
+		
+		// Try to load review-specific files in priority order
+		const reviewFiles = [
+			path.join(skillDir, "review-checklist.md"),
+			path.join(skillDir, "workflows", "Review.md"),
+			path.join(skillDir, "SKILL.md"),
+		];
+		
+		for (const filepath of reviewFiles) {
+			try {
+				const content = await fs.readFile(filepath, "utf-8");
+				const filename = path.relative(skillsDir, filepath);
+				
+				// For SKILL.md, try to extract review section
+				if (filepath.endsWith("SKILL.md")) {
+					const reviewSection = content.match(/## Review Best Practices[\s\S]*?(?=##\s+[A-Z]|$)/);
+					if (reviewSection) {
+						return `
+
+# Repository-Specific Review Guidelines: ${repo}
+
+From: ~/.config/claude/skills/${filename}
+
+${reviewSection[0]}
+`;
+					}
+				}
+				
+				// For other files, include the whole content
+				return `
+
+# Repository-Specific Review Guidelines: ${repo}
+
+From: ~/.config/claude/skills/${filename}
+
+${content}
+`;
+			} catch {
+				// Try next file
+				continue;
+			}
+		}
+		
+		// None found
+		return null;
+	} catch {
+		return null;
+	}
+}
+
 async function saveReviewMetadata(reviewData: {
 	type: string;
 	target: string;
@@ -977,6 +1134,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
 		const prompt = await buildReviewPrompt(pi, target);
 		const hint = getUserFacingHint(target);
 		const projectGuidelines = await loadProjectReviewGuidelines(ctx.cwd);
+		const repoRules = await loadRepositorySpecificRules(ctx.cwd);
 
 		// Combine the review rubric with the specific prompt
 		let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`;
@@ -985,6 +1143,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
 			fullPrompt += `\n\nThis project has additional instructions for code reviews:\n\n${projectGuidelines}`;
 		}
 
+		if (repoRules) {
+			fullPrompt += repoRules;
+		}
+
 		const modeHint = useFreshSession ? " (fresh session)" : "";
 		ctx.ui.notify(`Starting review: ${hint}${modeHint}`, "info");