Commit 93f3671a292e
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");