Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4bf1975
feat(review-memory): add feedback and proposal schema
alex-alecu May 22, 2026
7c51482
feat(review-memory): sync review subjects
alex-alecu May 22, 2026
9982082
feat(review-memory): ingest GitHub feedback signals
alex-alecu May 22, 2026
0bffe78
feat(review-memory): ingest GitLab feedback signals
alex-alecu May 22, 2026
12b55a1
feat(review-memory): aggregate feedback into proposals
alex-alecu May 22, 2026
7cfa8e7
feat(review-memory): expose proposal APIs
alex-alecu May 22, 2026
682218b
feat(review-memory): add memory tab UI
alex-alecu May 22, 2026
8023286
feat(review-memory): open REVIEW.md PRs and MRs
alex-alecu May 23, 2026
d3675c6
feat(review-memory): link review summaries to memory proposals
alex-alecu May 23, 2026
f2d2066
fix(review-memory): satisfy pre-push lint
alex-alecu May 23, 2026
97a5811
fix(review-memory): scope manual claim
alex-alecu May 25, 2026
6df6644
fix(review-memory): guard approval race
alex-alecu May 25, 2026
20813c3
fix(review-memory): drop unused cost
alex-alecu May 25, 2026
b026a40
Merge remote-tracking branch 'origin/main' into feat/review-memory-wo…
kilo-code-bot[bot] May 28, 2026
87b1400
Merge remote-tracking branch 'origin/main' into feat/review-memory-wo…
alex-alecu May 28, 2026
837087c
Merge remote-tracking branch 'origin/main' into feat/review-memory-wo…
alex-alecu May 28, 2026
242ddd4
fix(review-memory): make aggregation manual-only
alex-alecu May 28, 2026
6a5b855
chore(merge): merge origin main
alex-alecu May 28, 2026
e9bff4d
fix(review-memory): guard proposal edits
alex-alecu May 28, 2026
388a563
fix(review-memory): enforce retention window
alex-alecu May 28, 2026
d11b6ac
chore(merge): merge origin main
alex-alecu May 29, 2026
456dd41
fix(review-memory): remove duplicate prune
alex-alecu May 29, 2026
edabb4e
fix(review-memory): scope feedback subjects by owner
alex-alecu May 29, 2026
3381b16
fix(review-memory): bound retention cleanup
alex-alecu May 29, 2026
6968641
fix(review-memory): skip ignored GitLab webhook persistence
alex-alecu May 29, 2026
37d89d8
fix(review-memory): guard proposal rejection states
alex-alecu May 29, 2026
c696dde
fix(review-memory): require owner for change requests
alex-alecu May 29, 2026
ece7cad
fix(review-memory): use stored dashboard rollups
alex-alecu May 29, 2026
392f240
chore(merge): merge origin main
alex-alecu May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions apps/web/src/app/(app)/code-reviews/ReviewAgentPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { toast } from 'sonner';
import { ReviewConfigForm } from '@/components/code-reviews/ReviewConfigForm';
import { CodeReviewActionRequiredAlert } from '@/components/code-reviews/CodeReviewActionRequiredAlert';
import { CodeReviewJobsCard } from '@/components/code-reviews/CodeReviewJobsCard';
import { ReviewMemoryPanel } from '@/components/code-reviews/ReviewMemoryPanel';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { SetPageTitle } from '@/components/SetPageTitle';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Rocket, ExternalLink, Settings2, ListChecks } from 'lucide-react';
import { Brain, Rocket, ExternalLink, Settings2, ListChecks } from 'lucide-react';
import { useTRPC } from '@/lib/trpc/utils';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
Expand All @@ -20,19 +21,22 @@ import { GitLabLogo } from '@/components/auth/GitLabLogo';
import { GitHubLogo } from '@/components/auth/GitHubLogo';

type Platform = 'github' | 'gitlab';
type InnerTab = 'config' | 'jobs' | 'memory';

type ReviewAgentPageClientProps = {
userId: string;
userName: string;
successMessage?: string;
errorMessage?: string;
initialPlatform?: Platform;
initialTab?: InnerTab;
};

export function ReviewAgentPageClient({
successMessage,
errorMessage,
initialPlatform = 'github',
initialTab = 'config',
}: ReviewAgentPageClientProps) {
const trpc = useTRPC();
const router = useRouter();
Expand Down Expand Up @@ -162,8 +166,8 @@ export function ReviewAgentPageClient({
)}

{/* GitHub Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
<Tabs defaultValue={initialTab} className="w-full">
<TabsList className="grid w-full max-w-3xl grid-cols-3">
<TabsTrigger value="config" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Config
Expand All @@ -176,6 +180,14 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
Jobs
</TabsTrigger>
<TabsTrigger
value="memory"
className="flex items-center gap-2"
disabled={!isGitHubAppInstalled}
>
<Brain className="h-4 w-4" />
Memory
</TabsTrigger>
</TabsList>

<TabsContent value="config" className="mt-6 space-y-4">
Expand All @@ -196,6 +208,10 @@ export function ReviewAgentPageClient({
</Alert>
)}
</TabsContent>

<TabsContent value="memory" className="mt-6 space-y-4">
<ReviewMemoryPanel platform="github" />
</TabsContent>
</Tabs>
</TabsContent>

Expand Down Expand Up @@ -226,8 +242,8 @@ export function ReviewAgentPageClient({
)}

{/* GitLab Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
<Tabs defaultValue={initialTab} className="w-full">
<TabsList className="grid w-full max-w-3xl grid-cols-3">
<TabsTrigger value="config" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Config
Expand All @@ -240,6 +256,14 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
Jobs
</TabsTrigger>
<TabsTrigger
value="memory"
className="flex items-center gap-2"
disabled={!isGitLabConnected}
>
<Brain className="h-4 w-4" />
Memory
</TabsTrigger>
</TabsList>

<TabsContent value="config" className="mt-6 space-y-4">
Expand Down Expand Up @@ -275,6 +299,10 @@ export function ReviewAgentPageClient({
</Alert>
)}
</TabsContent>

<TabsContent value="memory" className="mt-6 space-y-4">
<ReviewMemoryPanel platform="gitlab" />
</TabsContent>
</Tabs>
</TabsContent>
</Tabs>
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/(app)/code-reviews/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { getUserFromAuthOrRedirect } from '@/lib/user/server';
import { ReviewAgentPageClient } from './ReviewAgentPageClient';

type ReviewAgentPageProps = {
searchParams: Promise<{ success?: string; error?: string; platform?: string }>;
searchParams: Promise<{ success?: string; error?: string; platform?: string; tab?: string }>;
};

export default async function PersonalReviewAgentPage({ searchParams }: ReviewAgentPageProps) {
const search = await searchParams;
const user = await getUserFromAuthOrRedirect('/users/sign_in?callbackPath=/code-reviews');
const platform = search.platform === 'gitlab' ? 'gitlab' : 'github';
const tab = search.tab === 'memory' || search.tab === 'jobs' ? search.tab : 'config';

return (
<ReviewAgentPageClient
Expand All @@ -17,6 +18,7 @@ export default async function PersonalReviewAgentPage({ searchParams }: ReviewAg
successMessage={search.success}
errorMessage={search.error}
initialPlatform={platform}
initialTab={tab}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { toast } from 'sonner';
import { ReviewConfigForm } from '@/components/code-reviews/ReviewConfigForm';
import { CodeReviewActionRequiredAlert } from '@/components/code-reviews/CodeReviewActionRequiredAlert';
import { CodeReviewJobsCard } from '@/components/code-reviews/CodeReviewJobsCard';
import { ReviewMemoryPanel } from '@/components/code-reviews/ReviewMemoryPanel';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { SetPageTitle } from '@/components/SetPageTitle';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Rocket, ExternalLink, Settings2, ListChecks } from 'lucide-react';
import { Brain, Rocket, ExternalLink, Settings2, ListChecks } from 'lucide-react';
import { useTRPC } from '@/lib/trpc/utils';
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link';
Expand All @@ -19,13 +20,15 @@ import { GitLabLogo } from '@/components/auth/GitLabLogo';
import { GitHubLogo } from '@/components/auth/GitHubLogo';

type Platform = 'github' | 'gitlab';
type InnerTab = 'config' | 'jobs' | 'memory';

type ReviewAgentPageClientProps = {
organizationId: string;
organizationName: string;
successMessage?: string;
errorMessage?: string;
initialPlatform?: Platform;
initialTab?: InnerTab;
};

export function ReviewAgentPageClient({
Expand All @@ -34,6 +37,7 @@ export function ReviewAgentPageClient({
successMessage,
errorMessage,
initialPlatform = 'github',
initialTab = 'config',
}: ReviewAgentPageClientProps) {
const trpc = useTRPC();
const router = useRouter();
Expand Down Expand Up @@ -175,8 +179,8 @@ export function ReviewAgentPageClient({
)}

{/* GitHub Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
<Tabs defaultValue={initialTab} className="w-full">
<TabsList className="grid w-full max-w-3xl grid-cols-3">
<TabsTrigger value="config" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Config
Expand All @@ -189,6 +193,14 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
Jobs
</TabsTrigger>
<TabsTrigger
value="memory"
className="flex items-center gap-2"
disabled={!isGitHubAppInstalled}
>
<Brain className="h-4 w-4" />
Memory
</TabsTrigger>
</TabsList>

<TabsContent value="config" className="mt-6 space-y-4">
Expand All @@ -209,6 +221,10 @@ export function ReviewAgentPageClient({
</Alert>
)}
</TabsContent>

<TabsContent value="memory" className="mt-6 space-y-4">
<ReviewMemoryPanel organizationId={organizationId} platform="github" />
</TabsContent>
</Tabs>
</TabsContent>

Expand Down Expand Up @@ -242,8 +258,8 @@ export function ReviewAgentPageClient({
)}

{/* GitLab Configuration Tabs */}
<Tabs defaultValue="config" className="w-full">
<TabsList className="grid w-full max-w-2xl grid-cols-2">
<Tabs defaultValue={initialTab} className="w-full">
<TabsList className="grid w-full max-w-3xl grid-cols-3">
<TabsTrigger value="config" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
Config
Expand All @@ -256,6 +272,14 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
Jobs
</TabsTrigger>
<TabsTrigger
value="memory"
className="flex items-center gap-2"
disabled={!isGitLabConnected}
>
<Brain className="h-4 w-4" />
Memory
</TabsTrigger>
</TabsList>

<TabsContent value="config" className="mt-6 space-y-4">
Expand Down Expand Up @@ -292,6 +316,10 @@ export function ReviewAgentPageClient({
</Alert>
)}
</TabsContent>

<TabsContent value="memory" className="mt-6 space-y-4">
<ReviewMemoryPanel organizationId={organizationId} platform="gitlab" />
</TabsContent>
</Tabs>
</TabsContent>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { ReviewAgentPageClient } from './ReviewAgentPageClient';

type ReviewAgentPageProps = {
params: Promise<{ id: string }>;
searchParams: Promise<{ success?: string; error?: string; platform?: string }>;
searchParams: Promise<{ success?: string; error?: string; platform?: string; tab?: string }>;
};

export default async function ReviewAgentPage({ params, searchParams }: ReviewAgentPageProps) {
const search = await searchParams;
const platform = search.platform === 'gitlab' ? 'gitlab' : 'github';
const tab = search.tab === 'memory' || search.tab === 'jobs' ? search.tab : 'config';

return (
<OrganizationByPageLayout
Expand All @@ -20,6 +21,7 @@ export default async function ReviewAgentPage({ params, searchParams }: ReviewAg
successMessage={search.success}
errorMessage={search.error}
initialPlatform={platform}
initialTab={tab}
/>
)}
/>
Expand Down
70 changes: 70 additions & 0 deletions apps/web/src/app/api/cron/cleanup-review-memory/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NextRequest } from 'next/server';

const mockPruneExpiredReviewMemoryData = jest.fn();

jest.mock('@/lib/config.server', () => ({
CRON_SECRET: 'cron-secret',
}));

jest.mock('@/lib/code-reviews/review-memory/db', () => ({
pruneExpiredReviewMemoryData: () => mockPruneExpiredReviewMemoryData(),
}));

import { GET } from './route';

function makeRequest(headers?: Record<string, string>) {
return new NextRequest('http://localhost:3000/api/cron/cleanup-review-memory', {
method: 'GET',
headers,
});
}

describe('GET /api/cron/cleanup-review-memory', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('rejects requests without cron authorization', async () => {
const response = await GET(makeRequest());

expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' });
expect(mockPruneExpiredReviewMemoryData).not.toHaveBeenCalled();
});

it('rejects requests with invalid cron authorization', async () => {
const response = await GET(makeRequest({ authorization: 'Bearer wrong-secret' }));

expect(response.status).toBe(401);
await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' });
expect(mockPruneExpiredReviewMemoryData).not.toHaveBeenCalled();
});

it('prunes review memory data when authorized', async () => {
mockPruneExpiredReviewMemoryData.mockResolvedValue({
cutoff: '2026-05-14T00:00:00.000Z',
proposalsDeleted: 1,
aggregationRunsDeleted: 2,
feedbackEventsDeleted: 3,
subjectsDeleted: 4,
aggregationStatesDeleted: 5,
});

const response = await GET(makeRequest({ authorization: 'Bearer cron-secret' }));

expect(response.status).toBe(200);
expect(mockPruneExpiredReviewMemoryData).toHaveBeenCalledTimes(1);
await expect(response.json()).resolves.toEqual({
success: true,
summary: {
cutoff: '2026-05-14T00:00:00.000Z',
proposalsDeleted: 1,
aggregationRunsDeleted: 2,
feedbackEventsDeleted: 3,
subjectsDeleted: 4,
aggregationStatesDeleted: 5,
},
timestamp: expect.any(String),
});
});
});
35 changes: 35 additions & 0 deletions apps/web/src/app/api/cron/cleanup-review-memory/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextResponse } from 'next/server';

import { CRON_SECRET } from '@/lib/config.server';
import { pruneExpiredReviewMemoryData } from '@/lib/code-reviews/review-memory/db';
import { sentryLogger } from '@/lib/utils.server';

if (!CRON_SECRET) {
throw new Error('CRON_SECRET is not configured in environment variables');
}

export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
const expectedAuth = `Bearer ${CRON_SECRET}`;
if (authHeader !== expectedAuth) {
sentryLogger(
'cron',
'warning'
)(
'SECURITY: Invalid CRON job authorization attempt: ' +
(authHeader ? 'Invalid authorization header' : 'Missing authorization header')
);
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const summary = await pruneExpiredReviewMemoryData();

return NextResponse.json(
{
success: true,
summary,
timestamp: new Date().toISOString(),
},
{ status: 200 }
);
}
Loading