diff --git a/src/selfhost/private-config.ts b/src/selfhost/private-config.ts index b3a12ccd83..87536d2677 100644 --- a/src/selfhost/private-config.ts +++ b/src/selfhost/private-config.ts @@ -84,15 +84,16 @@ export function makeLocalManifestReader(dir: string | undefined): RepoFocusManif }; } -/** Per-repo review-context candidate FOLDERS (relative to GITTENSORY_REPO_CONFIG_DIR): `{owner}__{repo}/review` then - * `{repo}/review`. Same owner/repo validation as localConfigCandidates; an invalid full name yields none. (#review-skills) */ +/** Per-repo review-context candidate FOLDERS (relative to GITTENSORY_REPO_CONFIG_DIR): only + * `{owner}__{repo}/review`. Review prompt material is private and must stay owner-scoped; an invalid full name + * yields none. (#review-skills) */ function reviewContextFolders(repoFullName: string): string[] { const slash = repoFullName.indexOf("/"); if (slash <= 0 || slash === repoFullName.length - 1 || slash !== repoFullName.lastIndexOf("/")) return []; const owner = repoFullName.slice(0, slash).toLowerCase(); const repo = repoFullName.slice(slash + 1).toLowerCase(); if (!GITHUB_OWNER_SEGMENT.test(owner) || !isSafeRepoSegment(repo)) return []; - return [join(`${owner}__${repo}`, "review"), join(repo, "review")]; + return [join(`${owner}__${repo}`, "review")]; } /** Parse a skill markdown file into {name, when, body}. YAML frontmatter (`---\nname:\nwhen:\n---`) is optional; name @@ -108,7 +109,7 @@ export function parseReviewSkill(filename: string, text: string): RepoReviewSkil } /** Build the container-local review-context reader over GITTENSORY_REPO_CONFIG_DIR, or null when the dir is unset. Per - * repo (first existing folder wins) reads `review/CLAUDE.md` (the guide) + every `review/skills/*.md` (rubric modules, + * repo reads owner-qualified `review/CLAUDE.md` (the guide) + every `review/skills/*.md` (rubric modules, * sorted). Missing files/dir degrade to nulls/empty; a per-file read error skips that file. (#review-skills) */ export function makeLocalReviewContextReader(dir: string | undefined): RepoReviewContextReader | null { const trimmed = (dir ?? "").trim(); diff --git a/test/unit/private-config.test.ts b/test/unit/private-config.test.ts index fcf8bf3766..fbed23354a 100644 --- a/test/unit/private-config.test.ts +++ b/test/unit/private-config.test.ts @@ -138,12 +138,17 @@ describe("makeLocalReviewContextReader (#review-skills)", () => { expect(ctx.skills.map((s) => s.name)).toEqual(["a-first", "second"]); // sorted by filename; .txt ignored }); - it("falls back to the bare repo-name folder; returns empty for a missing or invalid repo", async () => { + it("ignores bare repo-name review folders to keep private prompt material owner-scoped", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-review-")); mkdirSync(join(dir, "metagraphed", "review"), { recursive: true }); writeFileSync(join(dir, "metagraphed", "review", "CLAUDE.md"), "Bare-folder guide.\n"); const reader = makeLocalReviewContextReader(dir)!; - expect((await reader("JSONbored/metagraphed")).guide).toContain("Bare-folder guide."); + expect(await reader("JSONbored/metagraphed")).toEqual({ guide: null, skills: [] }); + }); + + it("returns empty for a missing or invalid repo", async () => { + const dir = mkdtempSync(join(tmpdir(), "gt-review-")); + const reader = makeLocalReviewContextReader(dir)!; expect(await reader("JSONbored/unknown-repo")).toEqual({ guide: null, skills: [] }); // no folder expect(await reader("owner/..")).toEqual({ guide: null, skills: [] }); // invalid repo segment → no candidates expect(await reader("noslash")).toEqual({ guide: null, skills: [] }); // invalid full name (no slash)