feat(triggers): add reviewScope config for review agent trigger#489
feat(triggers): add reviewScope config for review agent trigger#489
Conversation
Merge dev to main
Merge dev to main
|
✅ Review complete — see full review Verdict: Well-structured refactoring. Two should-fix issues around default behavior and UI/backend sync, plus minor stale docs. Key findings:
|
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Well-structured refactoring that consolidates three boolean trigger flags into a composable reviewScope array. The implementation is consistent across backend triggers, config schema, dashboard UI, and tests. Two issues worth addressing.
Code Issues
Should Fix
-
Default behavior change for
reviewRequestedtrigger — The old default forreviewRequestedwasfalse(opt-in, disabled). The new defaultreviewScope: ['reviewRequested']makes it enabled by default. This means existing projects with no trigger config will suddenly start firing the review agent when review is requested from a CASCADE persona. The PR description calls this a "safe fallback" but it's actually a behavioral change that could cause unexpected review agent runs on existing deployments. If this is intentional, it should be called out more prominently as a breaking change. If not, consider defaulting to[]to preserve the old opt-in semantics. -
Dashboard UI doesn't reflect the backend default — When
scmTriggersis{}(no config saved),extractRelevantTriggersreturns{ reviewScope: [] }(empty array) because the multi-select fallback is[]. But the backend default fromReviewScopeSchemais['reviewRequested']. This means the dashboard shows no options checked even though the backend is effectively operating withreviewRequestedenabled. If a user opens the review agent section and saves without changes, they'll write{ reviewScope: [] }and unintentionally disable the review-requested trigger. Consider adding adefaultArrayValuetoTriggerDefand using it as the fallback ingetMultiSelectValue/extractRelevantTriggers.
Minor
-
Stale JSDoc on
ReviewRequestedTriggerclass (src/triggers/github/review-requested.ts:16-17) — The class-level comment still says "Default: disabled (opt-in via trigger config). Enable by settinggithub.triggers.reviewRequested = true". Should be updated to reflect the newreviewScopesemantics and the new default (enabled). -
isReviewScopeEnabledhelper is exported and tested but unused in production code — All three trigger files usescope.includes(...)directly. Either use the helper consistently or remove it. -
resolveGitHubTriggerEnabledtype-safety gap — The function acceptskey: keyof GitHubTriggerConfig(which now includesreviewScope, an array) but returnsbooleanviareturn value as boolean. Nobody calls it with'reviewScope'today, but the type system won't prevent it.
| description: | ||
| 'Trigger review agent when review is requested from a CASCADE persona. Default disabled.', | ||
| 'Controls which PRs trigger the review agent. Select one or more modes. "Own PRs" = CI passes on implementer-authored PRs. "All PRs" = CI passes on any PR (also enables PR Opened for respond-to-review). "Review Requested" = review explicitly requested from a CASCADE persona.', | ||
| defaultValue: false, |
There was a problem hiding this comment.
The defaultValue: false here is only meaningful for checkbox-type triggers. For the multi-select, the fallback comes from getMultiSelectValue which returns [] when absent. This means the UI will show no options selected for projects without saved config, even though the backend default is ["reviewRequested"]. When the user saves, they will unintentionally write [] and disable the trigger.
Consider adding a defaultArrayValue field (e.g., ["reviewRequested"]) and using it in getMultiSelectValue / extractRelevantTriggers as the fallback when the value is absent.
There was a problem hiding this comment.
Fixed — added a defaultArrayValue field to TriggerDef (set to ['reviewRequested'] for the reviewScope trigger). getMultiSelectValue now accepts an optional defaultValue parameter, and both extractRelevantTriggers and handleSaveScm use t.defaultArrayValue as the fallback when no value is stored. This ensures the UI correctly shows 'Review Requested' checked for unconfigured projects, matching the backend default.
| ((parentObj as Record<string, unknown>)[child] as boolean) ?? t.defaultValue; | ||
| if (t.inputType === 'multi-select') { | ||
| const val = allTriggers[t.key]; | ||
| relevant[t.key] = Array.isArray(val) ? val : []; |
There was a problem hiding this comment.
When allTriggers has no reviewScope key (i.e. no triggers saved yet), this returns [] as the initial UI state. But the backend treats absent reviewScope as ["reviewRequested"]. This creates a UI/backend mismatch where: (1) the UI shows nothing selected, (2) saving without changes writes [], (3) that disables a trigger the backend would otherwise have enabled by default.
There was a problem hiding this comment.
Fixed — same defaultArrayValue approach resolves this. When allTriggers has no reviewScope key, extractRelevantTriggers now falls back to t.defaultArrayValue (['reviewRequested']) instead of []. The UI now correctly reflects the backend default for unconfigured projects, and saving without changes preserves the intended behavior.
nhopeatall
left a comment
There was a problem hiding this comment.
Review submitted — see inline comments and full summary above.
nhopeatall
left a comment
There was a problem hiding this comment.
LGTM with suggestions — see inline comments. The two should-fix items (default behavior change and UI/backend mismatch) are non-blocking since the new default is reasonable and the mismatch only affects first-save. The PR description clearly marks this as a breaking change.
|
@aaight address code review concerns |
Request AddressedWhat was askedAddress code review concerns from @nhopeatall's review. Changes Made
Verification
|
- Add defaultArrayValue to TriggerDef and use it as fallback in getMultiSelectValue/extractRelevantTriggers/handleSaveScm so the dashboard UI reflects the backend default ['reviewRequested'] for unconfigured projects (fixes UI/backend mismatch on first save) - Update stale JSDoc on ReviewRequestedTrigger to reflect new default (enabled via reviewScope, not opt-in boolean) - Use isReviewScopeEnabled helper consistently in all trigger files instead of direct .includes() calls - Narrow resolveGitHubTriggerEnabled key param to boolean-only trigger keys via GitHubBooleanTriggerKey type (prevents accidental use with reviewScope array field) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed all review concerns from @nhopeatall in commit 55f3bdb: Should Fix #2 — UI/backend default mismatch:
Minor #3 — Stale JSDoc:
Minor #4 — Unused
Minor #5 —
All checks pass: typecheck (0 errors), lint (0 errors, 1 pre-existing warning), tests (82 relevant tests pass; 6 pre-existing failures in |
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Well-structured refactoring that replaces three boolean flags with a composable reviewScope array. The implementation is clean, tests are comprehensive, and CI passes. One behavioral concern worth discussing.
Architecture & Design
-
[SHOULD_FIX] Breaking behavioral change for existing deployments: The old
checkSuiteSuccessdefaulted totrue, meaning CI-success auto-review on implementer PRs was enabled by default. The newreviewScopedefaults to['reviewRequested'], which does not include'own'. This means any existing project that relied on the default auto-review-after-CI behavior will silently stop getting auto-reviews after this deployment — their storedtriggersJSONB hascheckSuiteSuccess: true(or no key at all, relying on thetruedefault), butresolveReviewScopeignores that old key and returns['reviewRequested'].The PR description says "The Zod
.default(['reviewRequested'])provides a safe fallback" — this is safe from a crash perspective, but it silently changes behavior. Consider one of:- A data migration that sets
reviewScope: ['own', 'reviewRequested']for existing SCM integrations that hadcheckSuiteSuccess: true(or no explicit value, since the old default wastrue) - Changing the default to
['own', 'reviewRequested']to match the old behavior - Explicitly documenting this as a behavior-breaking change and notifying affected users
This may be intentional — if the team wants to move from opt-out to opt-in for CI-triggered reviews, the change is valid. But it should be a conscious decision, not an accidental side-effect.
- A data migration that sets
Code Issues
Should Fix
-
src/triggers/builtins.ts:58,69(not in diff) — Stale comments still reference the old trigger config: "disabled by default via trigger config (github.triggers.prOpened = false)" and "disabled by default via trigger config (github.triggers.reviewRequested = false)". These should be updated to referencereviewScope. -
src/api/routers/projects.ts:127(not in diff) — Theintegrations.upsertendpoint still validates triggers asz.record(z.boolean()).optional(). WhileupdateTriggerswas correctly updated to accept arrays, theupsertpath would reject a triggers payload containingreviewScopeas an array. This is minor since the dashboard usesupdateTriggersfor trigger changes, but it creates an inconsistency if anyone uses the upsert endpoint with triggers that include reviewScope.
Observations (no action needed)
- The
reviewScopeis resolved twice inCheckSuiteSuccessTrigger(once inmatches(), once inhandle()). Harmless but slightly redundant. - When
reviewScope: ['all']andpersonaIdentitiesis undefined, the reviewer-dedup check is effectively skipped (sincereviewerUsernamewill be undefined). This is acceptable sincepersonaIdentitiesshould always be available in production webhook contexts. - The
optionLabelfunction intrigger-toggles.tsxuses hardcoded string comparisons rather than deriving labels from a shared config. Fine for 3 options, but worth noting if more modes are added.
| */ | ||
| export const ReviewScopeSchema = z | ||
| .array(z.enum(['own', 'all', 'reviewRequested'])) | ||
| .default(['reviewRequested']); |
There was a problem hiding this comment.
Behavioral change worth confirming: The old checkSuiteSuccess defaulted to true (auto-review on CI pass was enabled by default). This new default of ["reviewRequested"] means existing projects without explicit reviewScope config will silently lose CI-triggered auto-reviews. Is this intentional? If so, worth calling out explicitly in the breaking change note. If not, consider defaulting to ["own", "reviewRequested"] to preserve backward compatibility.
There was a problem hiding this comment.
Good catch. This is intentional — we're moving from opt-out to opt-in for CI-triggered reviews. Added a migration note to CLAUDE.md explicitly documenting this behavioral change and instructing existing projects to set reviewScope: ['own', 'reviewRequested'] to restore the old default behavior.
Review CompleteVerdict: COMMENT — Clean implementation with one behavioral concern to discuss. Key FindingThe default changes existing behavior: projects that previously had auto-review on CI success (via the old default) will silently lose that behavior. See inline comment for details and options. Minor Issues
Overall the refactoring is well-executed — clean schema design, comprehensive tests, proper dashboard integration. |
nhopeatall
left a comment
There was a problem hiding this comment.
LGTM with comments — see inline review and prior summary comment for details on the behavioral change with the new default reviewScope.
|
@aaight address code review concerns |
Request AddressedWhat was askedAddress code review concerns from @nhopeatall's second review round. Changes Made
Verification
|
- Update stale comments in builtins.ts to reference reviewScope instead of old boolean flags (prOpened, reviewRequested) - Fix integrations.upsert endpoint to accept array values in triggers (consistent with updateTriggers endpoint) - Update upsertProjectIntegration function signature to accept broader trigger types (boolean | Record<string, boolean> | string[]) - Add migration note to CLAUDE.md documenting the behavioral change from checkSuiteSuccess:true default to reviewScope:['reviewRequested'] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed the second round of review concerns from @nhopeatall in commit e174681: Stale comments in
Breaking behavioral change documentation — Added a migration note to CLAUDE.md explicitly documenting that the old All checks pass: typecheck (0 errors), lint (0 errors, 1 pre-existing warning), tests (82 relevant tests pass; 6 pre-existing failures in |
|
🔍 Reviewing PR... |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Clean, well-structured change that replaces three boolean trigger flags with a single composable reviewScope array. The design is sound, the implementation is consistent across all layers (schema → triggers → API → dashboard), and tests are thorough.
Code Issues
Should Fix
- tests/unit/triggers/review-requested.test.ts:28 — Stale comment says
// Review-requested is opt-in, default disabledbut the new defaultreviewScope: ['reviewRequested']makes this trigger enabled by default. The test at line 80 correctly verifies the new behavior; the comment just needs updating.
Notes
- The breaking change from
checkSuiteSuccess: true(old default → auto-review implementer PRs on CI pass) toreviewScope: ['reviewRequested'](new default → review only on explicit request) is a meaningful behavioral shift for existing projects. The CLAUDE.md migration note documents this well. No runtime migration is needed sincetriggersis JSONB. - The
!Array.isArray(value)guard inupdateProjectIntegrationTriggerscorrectly ensures array values likereviewScopeare replaced wholesale rather than deep-merged — good. - The
personaIdentities?.reviewerchange incheck-suite-success.ts:139is a necessary fix sincepersonaIdentitiescan now be undefined whenreviewScope: ['all']skips the author gate butpersonaIdentitieswasn't provided.
| @@ -28,11 +28,11 @@ describe('ReviewRequestedTrigger', () => { | |||
| // Review-requested is opt-in, default disabled | |||
There was a problem hiding this comment.
Stale comment: with the new default reviewScope: ["reviewRequested"], this trigger is now enabled by default, not opt-in/disabled. The test at line 80 correctly verifies this; just the comment is misleading.
Summary
Implements configurable
reviewScopefor the review agent trigger, replacing the old boolean flags (checkSuiteSuccess,reviewRequested,prOpened) with a single composable array field.reviewScopefield onGitHubTriggerConfigSchema— array of'own' | 'all' | 'reviewRequested', default['reviewRequested']CheckSuiteSuccessTriggernow checksreviewScopeincludes'own'or'all'; when'all'the implementer-author gate is skippedReviewRequestedTriggernow checksreviewScopeincludes'reviewRequested'PROpenedTrigger(respond-to-review) now checksreviewScopeincludes'all'reviewScopecoverage scenariosreviewScopemodes and examplesBreaking change: Legacy boolean fields
checkSuiteSuccess,reviewRequested,prOpenedare removed from the schema. The Zod.default(['reviewRequested'])provides a safe fallback for any existing configs withoutreviewScope.Card: https://trello.com/c/699b4119c01eca9b4f6cc7ba
Test plan
npm test— all new tests pass (6 pre-existing failures unrelated to this PR)npx tsc --noEmit— zero type errorsnpm run lint— zero errors (1 pre-existing warning)["reviewRequested"](default) and verify CI pass doesn't auto-review["own"]and verify implementer PRs get auto-reviewed after CI["all"]and verify all PRs get reviewed; PR Opened trigger fires🤖 Generated with Claude Code