pierre-review 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/claude-review.js +2 -2
- package/dist/api/routes/insights.js +23 -0
- package/dist/api/routes/me.js +1 -0
- package/dist/api/routes/prs.js +16 -1
- package/dist/app.js +2 -0
- package/dist/config.js +9 -2
- package/dist/db/queries.js +266 -16
- package/dist/review/agent.js +5 -5
- package/dist/review/clone-manager.js +40 -0
- package/dist/review/review-manager.js +85 -28
- package/package.json +1 -1
- package/public/assets/index-CbNTBha6.js +1371 -0
- package/public/assets/index-CjBmEO6s.css +10 -0
- package/public/index.html +2 -2
- package/public/assets/index-DlSTGoNj.css +0 -10
- package/public/assets/index-sjQwmtK6.js +0 -1371
|
@@ -2,49 +2,73 @@ import { config } from '../config.js';
|
|
|
2
2
|
import { LOCAL_ACCOUNT_ID } from '../auth/account.js';
|
|
3
3
|
import { getLatestClaudeReview, getReviewPrContext } from '../db/queries.js';
|
|
4
4
|
import { runReview } from './agent.js';
|
|
5
|
-
import { insertQueuedReview, reconcileOrphanedReviews } from './persist.js';
|
|
6
|
-
// In-memory state (analogous to sync/sync-manager.ts). At most one review per PR
|
|
7
|
-
//
|
|
8
|
-
|
|
5
|
+
import { insertQueuedReview, markReviewCancelled, reconcileOrphanedReviews, } from './persist.js';
|
|
6
|
+
// In-memory state (analogous to sync/sync-manager.ts). At most one review per PR.
|
|
7
|
+
// `config.reviewConcurrency` bounds concurrent RUNNING reviews; extras wait in a
|
|
8
|
+
// FIFO `pending` queue and launch as slots free (so the user can bulk-trigger).
|
|
9
|
+
const running = new Set(); // prIds with a review actively running
|
|
10
|
+
const pending = []; // FIFO of queued reviews waiting for a slot
|
|
11
|
+
const claimed = new Set(); // prIds either running OR pending — the sync guard
|
|
9
12
|
const reviewIdByPr = new Map();
|
|
10
13
|
const progressByReview = new Map();
|
|
11
14
|
const controllers = new Map();
|
|
12
15
|
export async function startReview(prId, model, requestedMode, log) {
|
|
13
16
|
if (!config.claudeReviewEnabled)
|
|
14
17
|
return { ok: false, reason: 'disabled' };
|
|
15
|
-
|
|
18
|
+
// `claimed` covers BOTH running and queued, so a re-trigger of an in-flight or
|
|
19
|
+
// already-queued PR is rejected without a TOCTOU double-start.
|
|
20
|
+
if (claimed.has(prId))
|
|
16
21
|
return { ok: false, reason: 'already_running' };
|
|
17
|
-
|
|
22
|
+
// Runaway guard for bulk triggering: cap the queue depth.
|
|
23
|
+
if (pending.length >= config.reviewMaxQueued)
|
|
18
24
|
return { ok: false, reason: 'busy' };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// startReview for the same prId sees `running.has(prId)` immediately (no
|
|
22
|
-
// TOCTOU double-start). Roll it back on any early-bail / insert failure below.
|
|
23
|
-
running.add(prId);
|
|
25
|
+
// Reserve SYNCHRONOUSLY before any await; roll back on early-bail / insert failure.
|
|
26
|
+
claimed.add(prId);
|
|
24
27
|
let reviewId;
|
|
25
28
|
let ctx;
|
|
26
29
|
try {
|
|
27
30
|
ctx = await getReviewPrContext(prId, LOCAL_ACCOUNT_ID);
|
|
28
31
|
if (!ctx) {
|
|
29
|
-
|
|
32
|
+
claimed.delete(prId);
|
|
30
33
|
return { ok: false, reason: 'not_found' };
|
|
31
34
|
}
|
|
32
35
|
if (!ctx.headSha) {
|
|
33
|
-
|
|
36
|
+
claimed.delete(prId);
|
|
34
37
|
return { ok: false, reason: 'no_head' };
|
|
35
38
|
}
|
|
36
39
|
reviewId = await insertQueuedReview(prId, ctx.headSha, model, LOCAL_ACCOUNT_ID);
|
|
37
40
|
}
|
|
38
41
|
catch (err) {
|
|
39
|
-
|
|
42
|
+
claimed.delete(prId);
|
|
40
43
|
throw err;
|
|
41
44
|
}
|
|
42
|
-
const
|
|
45
|
+
const item = {
|
|
46
|
+
reviewId,
|
|
47
|
+
prId,
|
|
48
|
+
model,
|
|
49
|
+
requestedMode,
|
|
50
|
+
headSha: ctx.headSha,
|
|
51
|
+
ctx,
|
|
52
|
+
log,
|
|
53
|
+
};
|
|
54
|
+
reviewIdByPr.set(prId, reviewId);
|
|
55
|
+
if (running.size < config.reviewConcurrency) {
|
|
56
|
+
launch(item);
|
|
57
|
+
return { ok: true, reviewId, queued: false };
|
|
58
|
+
}
|
|
59
|
+
// No slot — leave it queued (the row's persisted status is already 'queued'); it
|
|
60
|
+
// launches from pump() when a running review finishes.
|
|
61
|
+
pending.push(item);
|
|
62
|
+
return { ok: true, reviewId, queued: true };
|
|
63
|
+
}
|
|
64
|
+
function launch(item) {
|
|
65
|
+
const { reviewId, prId, ctx } = item;
|
|
66
|
+
running.add(prId);
|
|
43
67
|
reviewIdByPr.set(prId, reviewId);
|
|
44
68
|
const controller = new AbortController();
|
|
45
69
|
controllers.set(reviewId, controller);
|
|
46
|
-
// The diff is fetched (and the mode decided) before any clone
|
|
47
|
-
//
|
|
70
|
+
// The diff is fetched (and the mode decided) before any clone, so the first real
|
|
71
|
+
// phase is always 'fetching_diff' — not 'cloning' (which a diff-only run skips).
|
|
48
72
|
progressByReview.set(reviewId, { phase: 'fetching_diff' });
|
|
49
73
|
void runReview({
|
|
50
74
|
reviewId,
|
|
@@ -56,32 +80,50 @@ export async function startReview(prId, model, requestedMode, log) {
|
|
|
56
80
|
title: ctx.title,
|
|
57
81
|
body: ctx.body,
|
|
58
82
|
baseRefName: ctx.baseRefName,
|
|
59
|
-
headSha,
|
|
60
|
-
model,
|
|
61
|
-
requestedMode,
|
|
83
|
+
headSha: item.headSha,
|
|
84
|
+
model: item.model,
|
|
85
|
+
requestedMode: item.requestedMode,
|
|
62
86
|
abortController: controller,
|
|
63
87
|
onProgress: (p) => progressByReview.set(reviewId, p),
|
|
64
88
|
})
|
|
65
89
|
.catch((err) => {
|
|
66
|
-
log.error(`claude review pr ${prId} failed: ${err instanceof Error ? err.message : err}`);
|
|
90
|
+
item.log.error(`claude review pr ${prId} failed: ${err instanceof Error ? err.message : err}`);
|
|
67
91
|
})
|
|
68
92
|
.finally(() => {
|
|
69
93
|
running.delete(prId);
|
|
70
94
|
reviewIdByPr.delete(prId);
|
|
71
95
|
controllers.delete(reviewId);
|
|
72
96
|
progressByReview.delete(reviewId);
|
|
97
|
+
claimed.delete(prId);
|
|
98
|
+
pump(); // a slot freed — start the next queued review
|
|
73
99
|
});
|
|
74
|
-
|
|
100
|
+
}
|
|
101
|
+
// Start as many queued reviews as there are free slots (FIFO).
|
|
102
|
+
function pump() {
|
|
103
|
+
while (running.size < config.reviewConcurrency && pending.length > 0) {
|
|
104
|
+
launch(pending.shift());
|
|
105
|
+
}
|
|
75
106
|
}
|
|
76
107
|
export function isReviewRunning(prId) {
|
|
77
108
|
return running.has(prId);
|
|
78
109
|
}
|
|
79
110
|
export function requestReviewCancel(prId) {
|
|
111
|
+
// Running → abort the SDK run (runReview's finally cleans up + pumps the queue).
|
|
80
112
|
const reviewId = reviewIdByPr.get(prId);
|
|
81
|
-
if (reviewId
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
if (running.has(prId) && reviewId != null) {
|
|
114
|
+
controllers.get(reviewId)?.abort();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// Queued (not yet started) → drop it from the queue and mark the row cancelled.
|
|
118
|
+
const idx = pending.findIndex((p) => p.prId === prId);
|
|
119
|
+
if (idx >= 0) {
|
|
120
|
+
const item = pending.splice(idx, 1)[0];
|
|
121
|
+
claimed.delete(prId);
|
|
122
|
+
reviewIdByPr.delete(prId);
|
|
123
|
+
void markReviewCancelled(item.reviewId).catch(() => { });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
85
127
|
}
|
|
86
128
|
// Live status when a review is in flight; otherwise the latest persisted run's
|
|
87
129
|
// status (or 'idle' if the PR was never reviewed).
|
|
@@ -94,13 +136,16 @@ export async function getReviewStatus(prId) {
|
|
|
94
136
|
progress: progressByReview.get(reviewId) ?? null,
|
|
95
137
|
};
|
|
96
138
|
}
|
|
139
|
+
const queued = pending.find((p) => p.prId === prId);
|
|
140
|
+
if (queued)
|
|
141
|
+
return { status: 'queued', reviewId: queued.reviewId, progress: null };
|
|
97
142
|
const latest = await getLatestClaudeReview(prId, LOCAL_ACCOUNT_ID);
|
|
98
143
|
if (!latest)
|
|
99
144
|
return { status: 'idle', reviewId: null, progress: null };
|
|
100
145
|
return { status: latest.status, reviewId: latest.id, progress: null };
|
|
101
146
|
}
|
|
102
|
-
// All reviews currently in flight (for the global progress banner),
|
|
103
|
-
// their PR coordinates.
|
|
147
|
+
// All reviews currently in flight OR queued (for the global progress banner),
|
|
148
|
+
// joined with their PR coordinates.
|
|
104
149
|
export async function listActiveReviews() {
|
|
105
150
|
const out = [];
|
|
106
151
|
for (const prId of running) {
|
|
@@ -120,6 +165,18 @@ export async function listActiveReviews() {
|
|
|
120
165
|
phase: progressByReview.get(reviewId)?.phase ?? null,
|
|
121
166
|
});
|
|
122
167
|
}
|
|
168
|
+
// Queued items use their stored ctx (no extra fetch); the banner shows them too.
|
|
169
|
+
for (const item of pending) {
|
|
170
|
+
out.push({
|
|
171
|
+
reviewId: item.reviewId,
|
|
172
|
+
prId: item.prId,
|
|
173
|
+
repoFullName: item.ctx.repoFullName,
|
|
174
|
+
prNumber: item.ctx.number,
|
|
175
|
+
prTitle: item.ctx.title,
|
|
176
|
+
status: 'queued',
|
|
177
|
+
phase: null,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
123
180
|
return out;
|
|
124
181
|
}
|
|
125
182
|
// Heal runs left 'running'/'queued' by a crash (our status is persisted).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pierre-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Alex Wakeman",
|