From 91f26dfe8f5696d057de186f77de65e0f51fb28e Mon Sep 17 00:00:00 2001 From: ultrahighsuper Date: Thu, 2 Jul 2026 06:34:35 -0500 Subject: [PATCH 1/2] feat(selfhost): let a review skill opt out via `enabled: false` frontmatter Add isReviewSkillEnabled(text): a self-host review skill can set `enabled: false` (or no/off/0) in its frontmatter to be omitted from the review context, so an operator can turn a rubric off without deleting the file. The local review-context reader skips disabled skills. Fully backward-compatible: a skill without an `enabled` key (every existing one) stays enabled, so behavior is unchanged unless the directive is explicitly set. Cover the parse vocabulary and the reader-level omission. --- src/selfhost/private-config.ts | 18 +++++++++++++++--- test/unit/private-config.test.ts | 24 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/selfhost/private-config.ts b/src/selfhost/private-config.ts index fdfa5d8e3d..c79b9738bb 100644 --- a/src/selfhost/private-config.ts +++ b/src/selfhost/private-config.ts @@ -110,10 +110,20 @@ export function parseReviewSkill(filename: string, text: string): RepoReviewSkil return { name, when, body }; } +/** True unless a skill's frontmatter explicitly disables it with `enabled: false` (or `no`/`off`/`0`). Absent or + * truthy `enabled` keeps the skill, so existing skills are unaffected — this only lets an operator turn a rubric + * OFF without deleting the file. Truthy vocabulary matches the codebase flag convention. (#review-skills) */ +export function isReviewSkillEnabled(text: string): boolean { + const head = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(text)?.[1] ?? ""; + const raw = /(?:^|\n)enabled:\s*(.+)/.exec(head)?.[1]?.trim().replace(/^["']|["']$/g, ""); + return raw === undefined ? true : /^(1|true|yes|on)$/i.test(raw); +} + /** 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/AGENTS.md` (Codex) or `review/CLAUDE.md` (Claude Code) as the - * guide + every `review/skills/*.md` rubric module, sorted. Missing files/dir degrade to nulls/empty; a per-file - * read error skips that file. (#review-skills) */ + * guide + every `review/skills/*.md` rubric module, sorted. A skill whose frontmatter sets `enabled: false` is + * omitted (turned off without deleting the file). 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(); if (!trimmed) return null; @@ -135,7 +145,9 @@ export function makeLocalReviewContextReader(dir: string | undefined): RepoRevie const entries = (await readdir(resolve(abs, "skills"))).filter((f) => f.toLowerCase().endsWith(".md")).sort(); for (const f of entries) { try { - skills.push(parseReviewSkill(f, await readFile(resolve(abs, "skills", f), "utf8"))); + const text = await readFile(resolve(abs, "skills", f), "utf8"); + if (!isReviewSkillEnabled(text)) continue; // `enabled: false` frontmatter disables a skill without deleting it + skills.push(parseReviewSkill(f, text)); } catch { // unreadable skill file → skip it } diff --git a/test/unit/private-config.test.ts b/test/unit/private-config.test.ts index 6acf7ed756..4028e8e793 100644 --- a/test/unit/private-config.test.ts +++ b/test/unit/private-config.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; -import { GLOBAL_CONFIG_CANDIDATES, localConfigCandidates, makeLocalManifestReader, makeLocalReviewContextReader, parseReviewSkill } from "../../src/selfhost/private-config"; +import { GLOBAL_CONFIG_CANDIDATES, isReviewSkillEnabled, localConfigCandidates, makeLocalManifestReader, makeLocalReviewContextReader, parseReviewSkill } from "../../src/selfhost/private-config"; import { loadRepoReviewContext, setLocalReviewContextReader } from "../../src/signals/focus-manifest-loader"; describe("localConfigCandidates (container-private config paths)", () => { @@ -125,6 +125,18 @@ describe("parseReviewSkill (#review-skills)", () => { }); }); +describe("isReviewSkillEnabled (#review-skills)", () => { + it("keeps a skill by default and honors an explicit enabled directive", () => { + expect(isReviewSkillEnabled("no frontmatter at all")).toBe(true); // no frontmatter → enabled + expect(isReviewSkillEnabled("---\nname: x\n---\nbody")).toBe(true); // frontmatter without `enabled` → enabled + expect(isReviewSkillEnabled("---\nenabled: true\n---\nbody")).toBe(true); + expect(isReviewSkillEnabled('---\nenabled: "on"\n---\nbody')).toBe(true); // quoted truthy stripped + expect(isReviewSkillEnabled("---\nenabled: false\n---\nbody")).toBe(false); + expect(isReviewSkillEnabled("---\nname: x\nenabled: no\n---\nbody")).toBe(false); + expect(isReviewSkillEnabled("---\nenabled: 0\n---\nbody")).toBe(false); // any non-truthy value disables + }); +}); + describe("makeLocalReviewContextReader (#review-skills)", () => { it("returns null when the dir is unset/blank", () => { expect(makeLocalReviewContextReader(undefined)).toBeNull(); @@ -147,6 +159,16 @@ describe("makeLocalReviewContextReader (#review-skills)", () => { expect(ctx.skills.map((s) => s.name)).toEqual(["a-first", "second"]); // sorted by filename; .txt ignored }); + it("omits a skill whose frontmatter sets enabled: false", async () => { + const dir = mkdtempSync(join(tmpdir(), "gt-review-")); + const rev = join(dir, "jsonbored__gittensory", "review"); + mkdirSync(join(rev, "skills"), { recursive: true }); + writeFileSync(join(rev, "skills", "a-active.md"), "---\nname: active\nwhen: always\n---\nActive rubric.\n"); + writeFileSync(join(rev, "skills", "b-disabled.md"), "---\nname: disabled\nenabled: false\n---\nParked rubric.\n"); + const ctx = await makeLocalReviewContextReader(dir)!("JSONbored/gittensory"); + expect(ctx.skills.map((s) => s.name)).toEqual(["active"]); // the disabled skill is dropped, not deleted + }); + it("falls back to legacy CLAUDE.md in the bare repo-name folder; returns empty for a missing or invalid repo", async () => { const dir = mkdtempSync(join(tmpdir(), "gt-review-")); mkdirSync(join(dir, "metagraphed", "review"), { recursive: true }); From 8d41273a40ec483806d2aace72bec1c02e30869e Mon Sep 17 00:00:00 2001 From: ultrahighsuper Date: Fri, 3 Jul 2026 11:02:17 +0900 Subject: [PATCH 2/2] fix(selfhost): ignore YAML inline comment on review-skill enabled directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `enabled: true # note` captured the whole value tail (`true # note`), failed the truthy test, and wrongly disabled the skill. Strip a trailing ` # …` comment before matching so an inline-commented directive reads as its bare value. --- src/selfhost/private-config.ts | 4 +++- test/unit/private-config.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/selfhost/private-config.ts b/src/selfhost/private-config.ts index c79b9738bb..9958db6a9f 100644 --- a/src/selfhost/private-config.ts +++ b/src/selfhost/private-config.ts @@ -115,7 +115,9 @@ export function parseReviewSkill(filename: string, text: string): RepoReviewSkil * OFF without deleting the file. Truthy vocabulary matches the codebase flag convention. (#review-skills) */ export function isReviewSkillEnabled(text: string): boolean { const head = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(text)?.[1] ?? ""; - const raw = /(?:^|\n)enabled:\s*(.+)/.exec(head)?.[1]?.trim().replace(/^["']|["']$/g, ""); + // Drop a YAML inline comment (` # …`) before matching, so `enabled: true # explicit` reads as `true`, not + // `true # explicit` (which would fail the truthy test and wrongly disable the skill). + const raw = /(?:^|\n)enabled:\s*(.+)/.exec(head)?.[1]?.replace(/\s+#.*$/, "").trim().replace(/^["']|["']$/g, ""); return raw === undefined ? true : /^(1|true|yes|on)$/i.test(raw); } diff --git a/test/unit/private-config.test.ts b/test/unit/private-config.test.ts index 4028e8e793..7d7d104ebf 100644 --- a/test/unit/private-config.test.ts +++ b/test/unit/private-config.test.ts @@ -135,6 +135,12 @@ describe("isReviewSkillEnabled (#review-skills)", () => { expect(isReviewSkillEnabled("---\nname: x\nenabled: no\n---\nbody")).toBe(false); expect(isReviewSkillEnabled("---\nenabled: 0\n---\nbody")).toBe(false); // any non-truthy value disables }); + it("ignores a YAML inline comment on the enabled directive", () => { + // A trailing ` # …` is a YAML comment, not part of the value — it must not flip a truthy directive to disabled. + expect(isReviewSkillEnabled("---\nenabled: true # temporarily explicit\n---\nbody")).toBe(true); + expect(isReviewSkillEnabled('---\nenabled: "on" # keep the rubric on\n---\nbody')).toBe(true); // comment after quoted value + expect(isReviewSkillEnabled("---\nenabled: false # parked for now\n---\nbody")).toBe(false); // still disables + }); }); describe("makeLocalReviewContextReader (#review-skills)", () => {