Skip to content

Commit e86933f

Browse files
authored
feat(review): upgrade inlineComments + fixHandoff to full config-as-code substitutes (#4116)
* feat(review): upgrade inlineComments + fixHandoff to full config-as-code substitutes An explicit review.inlineComments/fixHandoff: true|false in .gittensory.yml now fully controls each feature, bypassing the GITTENSORY_REVIEW_REPOS cutover allowlist entirely. Unset stays byte-identical to before (the allowlist alone was never sufficient on its own for either feature). The operator's env flag remains an absolute master kill-switch either way. Closes #4099. * fix(review): simplify inlineComments/fixHandoff parity, correct kill-switch docs - Revert the unnecessary boolean|null tri-state on shouldRequestInlineFindings and resolveReviewPromptOverrides's inlineComments field -- the function only ever checks '=== true', so null and undefined were functionally identical; collapsing back to a strict boolean (matching every sibling field) removes complexity that served no purpose, per gittensory-orb review feedback. - Make shouldEmitFixHandoff match shouldRequestInlineFindings exactly (both boolean | undefined, same shape) instead of drifting to boolean | null. - Clarify in both doc comments that the operator's env flag is an ABSOLUTE kill-switch never bypassable by per-repo config, consistent with every other converged feature (resolveConvergedFeature) -- addresses a linked- issue-satisfaction flag about apparent scope drift from the issue text.
1 parent 3cfe2a9 commit e86933f

7 files changed

Lines changed: 91 additions & 42 deletions

File tree

src/queue/processors.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6921,8 +6921,10 @@ export async function runAiReviewForAdvisory(
69216921
// positively scope the AI review. Empty ⇒ every non-excluded file is reviewed (byte-identical). Gate unaffected.
69226922
reviewPathFilters?: string[] | undefined;
69236923
// `.gittensory.yml` review.inline_comments (#inline-comments), resolved by the caller from the cached manifest
6924-
// (the per-repo toggle). ANDed here with the operator flag + cutover allowlist to decide whether to ASK the
6925-
// model for line-anchored inline findings. Absent/false ⇒ the reviewer prompt is byte-identical (no findings).
6924+
// (the per-repo toggle). Precedence (#4099): the operator flag is a master kill-switch, never bypassable by
6925+
// config; an explicit true/false here now fully controls the feature, bypassing the cutover allowlist; unset
6926+
// stays byte-identical to every repo's behavior before this change (the allowlist alone was never sufficient
6927+
// on its own). Absent ⇒ the reviewer prompt is byte-identical (no findings) for every repo untouched by this.
69266928
reviewInlineComments?: boolean | undefined;
69276929
// `.gittensory.yml` review.finding_categories (#1958), resolved by the caller from the cached manifest. ANDed
69286930
// here with reviewInlineComments (a category has nothing to categorize without an inline finding) to decide

src/review/fix-handoff.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
// Fix-handoff blocks (#2176, config slice for #1962) — copy-paste remediation guidance the reviewer can emit
2-
// ALONGSIDE the decision summary. Default OFF at every layer, mirroring the inline-comments precedent: the operator
3-
// flag GITTENSORY_REVIEW_FIX_HANDOFF, the per-repo convergence cutover allowlist, AND the per-repo `.gittensory.yml`
4-
// review.fixHandoff toggle are ALL ANDed before a fix-handoff block is ever emitted. This is the config/gate slice:
5-
// pure resolvers only — no emission/render here (that is a separate slice), so the gate/verdict is never touched.
6-
7-
import { isConvergenceRepoAllowed } from "./cutover-gate";
2+
// ALONGSIDE the decision summary. Default OFF: the operator flag GITTENSORY_REVIEW_FIX_HANDOFF is a master
3+
// kill-switch, and the per-repo `.gittensory.yml` review.fixHandoff toggle (#4099) fully controls activation by
4+
// itself when explicitly set — the per-repo convergence cutover allowlist no longer applies to this feature (an
5+
// unset manifest toggle preserves the ORIGINAL always-off default; it was never sufficient to be allowlisted
6+
// alone). This is the config/gate slice: pure resolvers only — no emission/render here (that is a separate
7+
// slice), so the gate/verdict is never touched.
88

99
/** True when the operator enabled fix-handoff globally. Flag-OFF (default) ⇒ the caller never emits fix-handoff
1010
* blocks. Truthy follows the codebase convention (same regex as isInlineCommentsEnabled). */
1111
export function isFixHandoffEnabled(env: { GITTENSORY_REVIEW_FIX_HANDOFF?: string | undefined }): boolean {
1212
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_FIX_HANDOFF ?? "");
1313
}
1414

15-
/** PURE: should the reviewer emit fix-handoff blocks for this PR? True ONLY when ALL THREE gates pass — the per-repo
16-
* `.gittensory.yml` toggle (`manifestToggle`), the operator flag, AND the convergence cutover allowlist — so the
17-
* feature is off by default at every layer. Mirrors shouldRequestInlineFindings, keeping the three-way gate in one
18-
* unit-testable place. */
15+
/** PURE (#4099): should the reviewer emit fix-handoff blocks for this PR? (1) The operator's
16+
* GITTENSORY_REVIEW_FIX_HANDOFF flag is an absolute MASTER KILL-SWITCH — off ⇒ always false, regardless of the
17+
* manifest, and no per-repo config can bypass it (consistent with every other converged feature — see
18+
* `resolveConvergedFeature` in `feature-activation.ts`). (2) An explicit per-repo `.gittensory.yml`
19+
* `review.fixHandoff` override (`true`/`false`) now FULLY controls the feature by itself — a repo can turn this
20+
* on without needing the GITTENSORY_REVIEW_REPOS cutover allowlist at all. (3) `manifestToggle` unset
21+
* (`undefined`) preserves this feature's ORIGINAL design exactly: being on the allowlist alone was never
22+
* sufficient, so this stays `false` regardless of the allowlist, byte-identical to every repo's behavior before
23+
* this change. Exactly mirrors `shouldRequestInlineFindings`'s shape and precedence. `repoFullName` is kept for
24+
* a stable call signature even though it's unused now that the allowlist no longer applies here. */
1925
export function shouldEmitFixHandoff(
26+
// GITTENSORY_REVIEW_REPOS is accepted (not just GITTENSORY_REVIEW_FIX_HANDOFF) purely for call-site signature
27+
// stability with existing callers/tests that pass a wider env object -- it's no longer read, see the doc
28+
// comment above.
2029
env: { GITTENSORY_REVIEW_FIX_HANDOFF?: string | undefined; GITTENSORY_REVIEW_REPOS?: string | undefined },
2130
repoFullName: string,
2231
manifestToggle: boolean | undefined,
2332
): boolean {
24-
return manifestToggle === true && isFixHandoffEnabled(env) && isConvergenceRepoAllowed(env, repoFullName);
33+
void repoFullName; // kept for call-site signature stability, see doc comment above
34+
if (!isFixHandoffEnabled(env)) return false;
35+
return manifestToggle === true;
2536
}

src/review/inline-comments.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// Quiet inline PR review comments (#inline-comments) — the CodeRabbit-style line-level layer ON TOP OF the
22
// decision summary. Posts the AI reviewer's line-anchored findings as a single NON-BLOCKING review (GitHub
33
// `event: COMMENT`, never REQUEST_CHANGES/APPROVE), so a contributor sees exactly what to fix on a resubmission
4-
// without the gate or its verdict ever changing. Default OFF at BOTH layers: the operator flag
5-
// GITTENSORY_REVIEW_INLINE_COMMENTS (+ the per-repo GITTENSORY_REVIEW_REPOS cutover allowlist) AND the per-repo
6-
// `.gittensory.yml` review.inline_comments toggle — the caller ANDs all three to decide whether to ASK the model
7-
// for inline findings AND passes the same resolved gate to the write boundary. Fully FAIL-SAFE: a finding whose
8-
// line is not a commentable line in the PR diff is dropped (GitHub 422s otherwise), and any API error degrades to
9-
// "no inline comments" — it NEVER throws and NEVER touches the gate.
4+
// without the gate or its verdict ever changing. Default OFF: the operator flag GITTENSORY_REVIEW_INLINE_COMMENTS
5+
// is a master kill-switch, and the per-repo `.gittensory.yml` review.inline_comments toggle (#4099) fully
6+
// controls activation by itself when explicitly set — the GITTENSORY_REVIEW_REPOS cutover allowlist no longer
7+
// applies to this feature (an unset manifest toggle preserves the ORIGINAL always-off default; it was never
8+
// sufficient to be allowlisted alone). Fully FAIL-SAFE: a finding whose line is not a commentable line in the PR
9+
// diff is dropped (GitHub 422s otherwise), and any API error degrades to "no inline comments" — it NEVER throws
10+
// and NEVER touches the gate.
1011

1112
import { createPullRequestReviewComments } from "../github/pr-actions";
12-
import { isConvergenceRepoAllowed } from "./cutover-gate";
1313
import { formatInlineCommentSeverityLabel } from "./inline-comment-label";
1414
import { resolveInlineCommentAnchor, rightLinesByPath } from "./inline-comment-range";
1515
import { addedLinesByPath, anchoredSuggestionBlock } from "./inline-suggestion-anchor";
@@ -28,16 +28,29 @@ export function isInlineCommentsEnabled(env: { GITTENSORY_REVIEW_INLINE_COMMENTS
2828
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_INLINE_COMMENTS ?? "");
2929
}
3030

31-
/** PURE: should the reviewer be asked to emit line-anchored inline findings for this PR? True ONLY when ALL THREE
32-
* gates pass — the per-repo `.gittensory.yml` toggle (`manifestToggle`), the operator flag, AND the cutover
33-
* allowlist — so the feature is off by default at every layer. Keeps the three-way gate in one unit-testable
34-
* place instead of inline in the review path. */
31+
/** PURE (#4099): should the reviewer be asked to emit line-anchored inline findings for this PR? (1) The
32+
* operator's GITTENSORY_REVIEW_INLINE_COMMENTS flag is an absolute MASTER KILL-SWITCH — off ⇒ always false,
33+
* regardless of the manifest, and no per-repo config can bypass it (consistent with every other converged
34+
* feature — see `resolveConvergedFeature` in `feature-activation.ts`). (2) An explicit per-repo
35+
* `.gittensory.yml` `review.inlineComments` override (`true`/`false`) now FULLY controls the feature by itself
36+
* — a repo can turn this on without needing the GITTENSORY_REVIEW_REPOS cutover allowlist at all. (3)
37+
* `manifestToggle` unset (`undefined`) preserves this feature's ORIGINAL design exactly: unlike
38+
* rag/reputation/safety/unifiedComment/grounding (which already fall back to the cutover allowlist when their
39+
* manifest field is unset), inline comments have always required an EXPLICIT per-repo opt-in — being on the
40+
* allowlist alone was never sufficient, so this stays `false` regardless of the allowlist, byte-identical to
41+
* every repo's behavior before this change. `repoFullName` is kept for a stable call signature even though it's
42+
* unused now that the allowlist no longer applies here. */
3543
export function shouldRequestInlineFindings(
44+
// GITTENSORY_REVIEW_REPOS is accepted (not just GITTENSORY_REVIEW_INLINE_COMMENTS) purely for call-site
45+
// signature stability with existing callers/tests that pass a wider env object -- it's no longer read, see
46+
// the doc comment above.
3647
env: { GITTENSORY_REVIEW_INLINE_COMMENTS?: string | undefined; GITTENSORY_REVIEW_REPOS?: string | undefined },
3748
repoFullName: string,
3849
manifestToggle: boolean | undefined,
3950
): boolean {
40-
return manifestToggle === true && isInlineCommentsEnabled(env) && isConvergenceRepoAllowed(env, repoFullName);
51+
void repoFullName; // kept for call-site signature stability, see doc comment above
52+
if (!isInlineCommentsEnabled(env)) return false;
53+
return manifestToggle === true;
4154
}
4255

4356
/** PURE (#1956): should a `suggestion` be rendered as a GitHub-native ` ```suggestion ` block? This is an

src/signals/focus-manifest.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ export function composeManifestReviewInstructions(instructions: string | null, t
249249
* (#review-profile / #review-tone / #review-security-focus / #review-path-instructions / #review-exclude-paths / #2043 / #selfhost-ai-model-override / #1956) */
250250
export function resolveReviewPromptOverrides(manifest: FocusManifest | null): { profile: ReviewProfile | null; tone: string | null; securityFocus: boolean; inlineComments: boolean; suggestions: boolean; changedFilesSummary: boolean; effortScore: boolean; impactMap: boolean; cultureProfile: boolean; findingCategories: boolean; inlineCommentsPerCategory: number | null; minFindingSeverity: ReviewFindingSeverity | null; maxFindings: MaxFindingsConfig; commentVerbosity: CommentVerbosity | null; pathInstructions: ReviewPathInstruction[]; instructions: string | null; excludePaths: string[]; pathFilters: string[]; selfHostAiModel: SelfHostAiModelConfig } {
251251
// inlineComments resolves to a strict boolean — true ONLY when the manifest explicitly set review.inline_comments:
252-
// true; null/false/absent ⇒ false. The caller ANDs this per-repo toggle with the operator flag + cutover allowlist.
252+
// true; null/false/absent ⇒ false. `shouldRequestInlineFindings` (#4099) only ever checks `=== true`, so null
253+
// and false are functionally identical to it — collapsing here (matching every sibling field below) is simpler
254+
// than plumbing a tri-state through for a distinction nothing downstream actually consumes.
253255
// securityFocus resolves the same way — true ONLY when the manifest explicitly set review.security_focus: true.
254256
// suggestions resolves the same way (#1956) — the caller further ANDs it with the already-resolved
255257
// inlineComments gate, since a suggestion has nothing to attach to without an inline comment.

test/unit/focus-manifest.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3039,7 +3039,11 @@ describe("resolveReviewPathInstructions (#review-path-instructions)", () => {
30393039
it("resolveReviewPromptOverrides: non-null manifest passes the config through; null manifest → defaults", () => {
30403040
const manifest = parseFocusManifest({ review: { profile: "chill", security_focus: true, inline_comments: true, suggestions: true, changed_files_summary: true, effort_score: true, impact_map: true, culture_profile: true, finding_categories: true, comment_verbosity: "detailed", path_instructions: [{ path: "src/**", instructions: "be strict" }], instructions: "Follow our async-error conventions.", exclude_paths: ["**/*.lock"], path_filters: ["src/**", "!src/generated/**"] } });
30413041
expect(resolveReviewPromptOverrides(manifest)).toEqual({ profile: "chill", tone: null, securityFocus: true, inlineComments: true, suggestions: true, changedFilesSummary: true, effortScore: true, impactMap: true, cultureProfile: true, findingCategories: true, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: "detailed", pathInstructions: [{ path: "src/**", instructions: "be strict" }], instructions: "Follow our async-error conventions.", excludePaths: ["**/*.lock"], pathFilters: ["src/**", "!src/generated/**"], selfHostAiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG } });
3042-
// A null manifest (load failure) yields the byte-identical defaults; inline comments + suggestions + changed-files summary + effort score + impact map + culture profile + finding categories + security focus default OFF.
3042+
// A null manifest (load failure) yields the byte-identical defaults; inline comments + suggestions +
3043+
// changed-files summary + effort score + impact map + culture profile + finding categories + security focus
3044+
// all default OFF (strict false) — inlineComments collapses the same way as every sibling flag on this
3045+
// object (#4099: shouldRequestInlineFindings only ever checks `=== true`, so null/false/absent are
3046+
// functionally identical to it; no tri-state needed here).
30433047
expect(resolveReviewPromptOverrides(null)).toEqual({ profile: null, tone: null, securityFocus: false, inlineComments: false, suggestions: false, changedFilesSummary: false, effortScore: false, impactMap: false, cultureProfile: false, findingCategories: false, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], selfHostAiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG } });
30443048
// An explicit false / absent toggle both resolve to the strict-boolean false.
30453049
expect(resolveReviewPromptOverrides(parseFocusManifest({ review: { inline_comments: false } })).inlineComments).toBe(false);

test/unit/inline-comments.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@ describe("isInlineCommentsEnabled (#inline-comments)", () => {
2020
});
2121
});
2222

23-
describe("shouldRequestInlineFindings (#inline-comments)", () => {
23+
describe("shouldRequestInlineFindings (#inline-comments / #4099)", () => {
2424
const on = { GITTENSORY_REVIEW_INLINE_COMMENTS: "true", GITTENSORY_REVIEW_REPOS: "acme/widgets" };
25-
it("requires ALL THREE gates: the per-repo manifest toggle, the operator flag, and the cutover allowlist", () => {
25+
it("operator flag is a master kill-switch — off ⇒ always false regardless of the manifest toggle", () => {
26+
expect(shouldRequestInlineFindings({ GITTENSORY_REVIEW_REPOS: "acme/widgets" }, "acme/widgets", true)).toBe(false);
27+
expect(shouldRequestInlineFindings({}, "acme/widgets", true)).toBe(false);
28+
});
29+
30+
it("REGRESSION (#4099): unset manifest toggle stays false regardless of the cutover allowlist — byte-identical to before this change (being allowlisted was never sufficient on its own)", () => {
31+
expect(shouldRequestInlineFindings(on, "acme/widgets", undefined)).toBe(false);
32+
expect(shouldRequestInlineFindings(on, "other/repo", undefined)).toBe(false);
33+
});
34+
35+
it("(#4099) an explicit manifest toggle: true fully controls the feature, even for a repo NOT on the cutover allowlist", () => {
2636
expect(shouldRequestInlineFindings(on, "acme/widgets", true)).toBe(true);
27-
expect(shouldRequestInlineFindings(on, "acme/widgets", false)).toBe(false); // manifest toggle off
28-
expect(shouldRequestInlineFindings(on, "acme/widgets", undefined)).toBe(false); // manifest toggle absent
29-
expect(shouldRequestInlineFindings({ GITTENSORY_REVIEW_REPOS: "acme/widgets" }, "acme/widgets", true)).toBe(false); // operator flag off
30-
expect(shouldRequestInlineFindings(on, "other/repo", true)).toBe(false); // repo not allowlisted
37+
expect(shouldRequestInlineFindings(on, "other/repo", true)).toBe(true);
38+
});
39+
40+
it("(#4099) an explicit manifest toggle: false forces the feature off, even for an allowlisted repo", () => {
41+
expect(shouldRequestInlineFindings(on, "acme/widgets", false)).toBe(false);
3142
});
3243
});
3344

test/unit/review-fix-handoff.test.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,27 @@ describe("review.fixHandoff config toggle (#2176)", () => {
3131
});
3232
});
3333

34-
describe("fix-handoff env kill-switch + resolver (#2176)", () => {
34+
describe("fix-handoff env kill-switch + resolver (#2176 / #4099)", () => {
3535
it("isFixHandoffEnabled: only truthy env values enable", () => {
3636
for (const v of ["1", "true", "yes", "on", "TRUE"]) expect(isFixHandoffEnabled({ GITTENSORY_REVIEW_FIX_HANDOFF: v })).toBe(true);
3737
for (const v of ["0", "false", "off", "", undefined]) expect(isFixHandoffEnabled({ GITTENSORY_REVIEW_FIX_HANDOFF: v })).toBe(false);
3838
});
3939

40-
it("shouldEmitFixHandoff: true ONLY when manifest toggle AND env flag AND cutover allowlist all pass", () => {
41-
// all three on
40+
it("operator flag is a master kill-switch — off ⇒ always false regardless of the manifest toggle", () => {
41+
expect(shouldEmitFixHandoff({ GITTENSORY_REVIEW_FIX_HANDOFF: "0", GITTENSORY_REVIEW_REPOS: ON }, ON, true)).toBe(false);
42+
});
43+
44+
it("REGRESSION (#4099): unset manifest toggle stays false regardless of the cutover allowlist — byte-identical to before this change (being allowlisted was never sufficient on its own)", () => {
45+
expect(shouldEmitFixHandoff(ALLOW, ON, undefined)).toBe(false);
46+
expect(shouldEmitFixHandoff({ GITTENSORY_REVIEW_FIX_HANDOFF: "1", GITTENSORY_REVIEW_REPOS: "other/repo" }, ON, undefined)).toBe(false);
47+
});
48+
49+
it("(#4099) an explicit manifest toggle: true fully controls the feature, even for a repo NOT on the cutover allowlist", () => {
4250
expect(shouldEmitFixHandoff(ALLOW, ON, true)).toBe(true);
43-
// manifest toggle off / undefined
51+
expect(shouldEmitFixHandoff({ GITTENSORY_REVIEW_FIX_HANDOFF: "1", GITTENSORY_REVIEW_REPOS: "other/repo" }, ON, true)).toBe(true);
52+
});
53+
54+
it("(#4099) an explicit manifest toggle: false forces the feature off, even for an allowlisted repo", () => {
4455
expect(shouldEmitFixHandoff(ALLOW, ON, false)).toBe(false);
45-
expect(shouldEmitFixHandoff(ALLOW, ON, undefined)).toBe(false);
46-
// env flag off
47-
expect(shouldEmitFixHandoff({ GITTENSORY_REVIEW_FIX_HANDOFF: "0", GITTENSORY_REVIEW_REPOS: ON }, ON, true)).toBe(false);
48-
// repo not on the cutover allowlist
49-
expect(shouldEmitFixHandoff({ GITTENSORY_REVIEW_FIX_HANDOFF: "1", GITTENSORY_REVIEW_REPOS: "other/repo" }, ON, true)).toBe(false);
5056
});
5157
});

0 commit comments

Comments
 (0)