limn-review 0.1.125 → 0.1.126
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/plugins/rate-limit.js +10 -0
- package/dist/coding/ai-fix/auto-fix.js +6 -0
- package/dist/coding/ai-fix/manager.js +8 -1
- package/dist/coding/ai-fix/review-seed.js +17 -13
- package/dist/config.js +8 -0
- package/dist/db/ci-review-prune.js +22 -0
- package/dist/db/erase-account.js +11 -0
- package/dist/db/migrations/0082_ci_reviews.sql +73 -0
- package/dist/db/migrations/0083_workspace_auto_fix.sql +9 -0
- package/dist/db/migrations/meta/_journal.json +14 -0
- package/dist/db/migrations-pg/0069_ci_reviews.sql +65 -0
- package/dist/db/migrations-pg/0070_workspace_auto_fix.sql +3 -0
- package/dist/db/migrations-pg/meta/_journal.json +14 -0
- package/dist/db/my-turn-dismissals.js +8 -11
- package/dist/db/queries.js +43 -45
- package/dist/db/retention.js +3 -0
- package/dist/db/schema.pg.js +91 -1
- package/dist/db/schema.sqlite.js +111 -0
- package/dist/review/agentic.js +15 -2
- package/dist/review/ci-review/agent.js +143 -0
- package/dist/review/ci-review/currency.js +143 -0
- package/dist/review/ci-review/manager.js +363 -0
- package/dist/review/ci-review/persist.js +486 -0
- package/dist/review/ci-review/prepare.js +49 -0
- package/dist/review/ci-review/prompts.js +103 -0
- package/dist/review/ci-review/reconcile.js +78 -0
- package/dist/review/ci-review/routes.js +197 -0
- package/dist/review/ci-review/schema.js +44 -0
- package/dist/review/ci-review/sweep.js +172 -0
- package/dist/review/claude-review/auto-settings.js +63 -21
- package/dist/review/claude-review/auto.js +42 -54
- package/dist/review/claude-review/ci-failures.js +0 -42
- package/dist/review/claude-review/manager.js +40 -70
- package/dist/review/claude-review/persist.js +149 -5
- package/dist/review/claude-review/prompts.js +42 -19
- package/dist/review/claude-review/settled-by-reply.js +146 -0
- package/dist/review/claude-review/specialists.js +1 -1
- package/dist/review/clone-hygiene.js +1 -1
- package/dist/review/clone-manager.js +24 -1
- package/dist/review/post-review.js +19 -2
- package/dist/review/schema.js +9 -29
- package/dist/review/submit-map.js +5 -3
- package/dist/review/ticket-review/sweep.js +56 -65
- package/dist/shared/types.js +13 -0
- package/package.json +1 -1
- package/public/assets/{AiFixTab-B1OazE4O.js → AiFixTab-CzugrcNK.js} +2 -2
- package/public/assets/ClaudeReviewTab-OGu_QicR.js +16 -0
- package/public/assets/{PrBotBehaviourTab-h7Nujxom.js → PrBotBehaviourTab-Cc1F4O6z.js} +1 -1
- package/public/assets/index-1ieo_9fr.js +77 -0
- package/public/assets/index-C9m8ITkB.css +1 -0
- package/public/index.html +2 -2
- package/public/assets/ClaudeReviewTab-7GZdYcOv.js +0 -8
- package/public/assets/index-DTRruvsb.css +0 -1
- package/public/assets/index-q7WCDbAA.js +0 -77
|
@@ -468,6 +468,16 @@ function tierFor(method, path) {
|
|
|
468
468
|
return [TIERS.githubWrite];
|
|
469
469
|
if (path.startsWith('/api/ticket-reviews'))
|
|
470
470
|
return [TIERS.read];
|
|
471
|
+
// CI REVIEW (review/ci-review/routes.ts), matched EXPLICITLY by exact path for the same reason:
|
|
472
|
+
// POST /api/ci-reviews starts an agent run (reads the head's checks + logs) → ai + ai_hourly
|
|
473
|
+
// POST /api/ci-reviews/states batched DB read of the SYNCED head + failing names → read
|
|
474
|
+
// GET …/:id, …/:id/stream, /api/prs/:id/ci-review → read
|
|
475
|
+
if (path === '/api/ci-reviews' && mutating)
|
|
476
|
+
return [TIERS.ai, TIERS.aiHourly];
|
|
477
|
+
if (path === '/api/ci-reviews/states')
|
|
478
|
+
return [TIERS.read];
|
|
479
|
+
if (path.startsWith('/api/ci-reviews'))
|
|
480
|
+
return [TIERS.read];
|
|
471
481
|
if (/^\/api\/claude-reviews\/[^/]+\/chat$/.test(path)) {
|
|
472
482
|
return mutating ? [TIERS.ai, TIERS.aiHourly] : [TIERS.read];
|
|
473
483
|
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// 0. the PR's author IS the account's own GitHub user (pull_requests.author_id → users.github_login
|
|
7
7
|
// vs accounts.github_login, case-insensitive). Anyone else's PR: nothing happens and nothing is
|
|
8
8
|
// recorded — fixing someone else's branch is never automatic;
|
|
9
|
+
// ½. `off` — auto AI Fix is switched off for the workspace holding the PR's repo
|
|
10
|
+
// (`workspaces.auto_fix_enabled`, migration 0083 / pg 0070, Settings →
|
|
11
|
+
// Auto review). ON by default; a repo with no membership row reads ON;
|
|
9
12
|
// 1. `nothing_to_fix` — the review's seed is empty (the same refusal as the button's NothingToFix);
|
|
10
13
|
// 2. `head_moved` — the PR's synced head is no longer the reviewed one (the next review decides);
|
|
11
14
|
// 3. `fix_in_progress` — a fix for this PR (auto or manual) is queued or running;
|
|
@@ -33,6 +36,7 @@ import { AUTO_FIX_DAILY_CAP, CLAUDE_REVIEW_MODELS, DEFAULT_AI_FIX_MODEL, } from
|
|
|
33
36
|
import { isFixRunning, loadReviewSeed, startReviewFix } from './manager.js';
|
|
34
37
|
import { parseChangeReport, parseReviewItems } from './persist.js';
|
|
35
38
|
import { getFixPrContext } from './pr-context.js';
|
|
39
|
+
import { readWorkspaceAutoFixForPr } from '../../review/claude-review/auto-settings.js';
|
|
36
40
|
// At most AUTO_FIX_DAILY_CAP (shared) AUTO fixes per PR in any rolling AUTO_FIX_WINDOW_MS.
|
|
37
41
|
export { AUTO_FIX_DAILY_CAP };
|
|
38
42
|
export const AUTO_FIX_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
@@ -119,6 +123,8 @@ export async function maybeStartAutoFix(ctx, input, deps = defaultDeps, nowMs =
|
|
|
119
123
|
try {
|
|
120
124
|
if (!(await prAuthorIsAccount(ctx, accountId, prId)))
|
|
121
125
|
return { status: 'not_own' };
|
|
126
|
+
if (!(await readWorkspaceAutoFixForPr(ctx, accountId, prId)))
|
|
127
|
+
return skip('off');
|
|
122
128
|
const loaded = await deps.loadReviewSeed(ctx, { accountId, prId, reviewId });
|
|
123
129
|
if (!loaded)
|
|
124
130
|
return skip('not_started');
|
|
@@ -4,6 +4,7 @@ import { getClaudeReviewById } from '../../review/claude-review/persist.js';
|
|
|
4
4
|
import { pickReviewNonce } from '../../review/claude-review/prompts.js';
|
|
5
5
|
import { buildReviewSeed } from './review-seed.js';
|
|
6
6
|
import { getOwnedTicketItemsForPr } from '../../review/ticket-review/persist.js';
|
|
7
|
+
import { getFixableCiItemsForPr } from '../../review/ci-review/persist.js';
|
|
7
8
|
import { buildFixSystemPrompt, buildFixUserPrompt } from './prompts.js';
|
|
8
9
|
import { insertQueuedFix, markFixCancelled, markFixFailed, markFixRunning, reconcileOrphanedFixes, saveFixSuccess, } from './persist.js';
|
|
9
10
|
// In-memory job manager for AI Fix's agentic fixer (CORE, free, local-only — it left the plugin
|
|
@@ -102,7 +103,13 @@ export async function loadReviewSeed(ctx, input) {
|
|
|
102
103
|
return [];
|
|
103
104
|
})
|
|
104
105
|
: [];
|
|
105
|
-
|
|
106
|
+
// The CI review's fixable items at the PR's CURRENT head (manual and auto fixes alike). A failed
|
|
107
|
+
// read costs the CI items only.
|
|
108
|
+
const ciItems = await getFixableCiItemsForPr(ctx, input.accountId, input.prId).catch((err) => {
|
|
109
|
+
ctx.log.warn({ err }, 'ai-fix: loading CI review items failed');
|
|
110
|
+
return [];
|
|
111
|
+
});
|
|
112
|
+
return { review, seed: buildReviewSeed(review, { nonce: pickReviewNonce, ticketItems, ciItems }) };
|
|
106
113
|
}
|
|
107
114
|
catch (err) {
|
|
108
115
|
ctx.log.warn({ err }, 'ai-fix: loading the review seed failed');
|
|
@@ -15,7 +15,9 @@ import { isThreadToFix } from '../../shared/index.js';
|
|
|
15
15
|
// addressed — unless this run re-raised it as a finding (then F covers it).
|
|
16
16
|
// T<n> another reviewer's thread the review judged still needs a fix (shared
|
|
17
17
|
// `isThreadToFix`: valid / partly valid AND not / partly addressed).
|
|
18
|
-
// C<n> a CI failure the review judged fixable in this PR
|
|
18
|
+
// C<n> a CI failure the CI REVIEW (review/ci-review/) explained and judged fixable in this PR —
|
|
19
|
+
// from its latest succeeded run AT THE PR'S CURRENT HEAD (`ciItems`, the caller's
|
|
20
|
+
// `getFixableCiItemsForPr`), never from the code review's own (legacy) `ciFailures`.
|
|
19
21
|
// S<t>-AC<n> an acceptance criterion of ticket t judged not met or partly met, and
|
|
20
22
|
// S<t>-M<n> something ticket t asked for that is missing — BOTH from the TICKET review
|
|
21
23
|
// (review/ticket-review/), never from the PR review, and ONLY the items whose owner is
|
|
@@ -72,21 +74,21 @@ function mkItem(ref, kind, title, over = {}) {
|
|
|
72
74
|
...over,
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
|
-
// ---- CI failures (the review's
|
|
76
|
-
// Only a DIAGNOSED failure
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return [];
|
|
82
|
-
return raw.filter((f) => f != null && f.status === 'diagnosed' && f.fixableInPr === true);
|
|
77
|
+
// ---- CI failures (the CI review's items) ----
|
|
78
|
+
// Only a DIAGNOSED failure judged fixable in this PR (`fixableInPr === true`). Anything else —
|
|
79
|
+
// infrastructure, flaky, unclear, not checked — is not the fixer's. The code review's own legacy
|
|
80
|
+
// `ciFailures` are NEVER read: the CI review replaced them, and they describe an older head.
|
|
81
|
+
export function fixableCiItems(items) {
|
|
82
|
+
return items.filter((f) => f != null && f.status === 'diagnosed' && f.fixableInPr === true);
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Collect every fixable item of a review, refs assigned. Pure — the review is the stored run.
|
|
86
86
|
*/
|
|
87
87
|
export function collectReviewItems(review,
|
|
88
88
|
// The ticket review's items THIS PR owns. A MANUAL fix only; absent/[] ⇒ none (an auto fix).
|
|
89
|
-
ticketItems = []
|
|
89
|
+
ticketItems = [],
|
|
90
|
+
// The CI review's items at the PR's current head (both manual and auto fixes); absent/[] ⇒ none.
|
|
91
|
+
ciItems = []) {
|
|
90
92
|
const out = [];
|
|
91
93
|
// F — the review's findings. A RE-RAISE (`priorFindingId` set) left out only because the same
|
|
92
94
|
// comment is already on this commit (follow-up.ts `isAlreadyOnThisCommit`) is NOT a reader's
|
|
@@ -161,13 +163,15 @@ ticketItems = []) {
|
|
|
161
163
|
});
|
|
162
164
|
});
|
|
163
165
|
// C — CI failures fixable in this PR.
|
|
164
|
-
|
|
166
|
+
fixableCiItems(ciItems).forEach((f, i) => {
|
|
165
167
|
const ref = `C${i + 1}`;
|
|
166
|
-
const first = f.relatedFiles?.[0] ?? null;
|
|
168
|
+
const first = f.path ? { path: f.path, line: f.line } : (f.relatedFiles?.[0] ?? null);
|
|
167
169
|
const body = [
|
|
168
170
|
`Check: ${f.checkName}${f.step ? ` (step: ${f.step})` : ''}`,
|
|
169
171
|
f.cause ? `Cause: ${f.cause}` : '',
|
|
170
172
|
f.explanation ? `Detail: ${f.explanation}` : '',
|
|
173
|
+
f.path ? `Where: ${where(f.path, f.line)}` : '',
|
|
174
|
+
f.suggestion ? `Suggested change: ${f.suggestion}` : '',
|
|
171
175
|
f.relatedFiles?.length
|
|
172
176
|
? `Related files: ${f.relatedFiles.map((r) => where(r.path, r.line)).join(', ')}`
|
|
173
177
|
: '',
|
|
@@ -226,7 +230,7 @@ function fencedTexts(items, summary) {
|
|
|
226
230
|
*/
|
|
227
231
|
export function buildReviewSeed(review, opts) {
|
|
228
232
|
const budget = opts.budgetChars ?? REVIEW_SEED_CHAR_BUDGET;
|
|
229
|
-
const all = collectReviewItems(review, opts.ticketItems ?? []);
|
|
233
|
+
const all = collectReviewItems(review, opts.ticketItems ?? [], opts.ciItems ?? []);
|
|
230
234
|
if (all.length === 0)
|
|
231
235
|
return { items: [], sentRefs: [], text: '' };
|
|
232
236
|
const summary = clip(review.userBody?.trim() || review.summary?.trim() || '', SUMMARY_MAX);
|
package/dist/config.js
CHANGED
|
@@ -539,6 +539,14 @@ export const config = {
|
|
|
539
539
|
// AUTOMATIC ticket reviews (trigger 'auto' / 'cascade') per workspace per UTC day. Not shared with
|
|
540
540
|
// the PR review's own auto cap. Manual runs never count. Default = shared TICKET_REVIEW_DAILY_CAP.
|
|
541
541
|
ticketReviewDailyCap: intFromEnv('TICKET_REVIEW_DAILY_CAP', 20),
|
|
542
|
+
// ---- CI review (one run per (PR, head, failing-check set) — review/ci-review/) ----
|
|
543
|
+
// Its OWN spend cap and turn cap: a CI run reads log excerpts and the files they point at, nothing
|
|
544
|
+
// more. Env only — no route sets it.
|
|
545
|
+
ciReviewBudgetUsd: floatFromEnv('CI_REVIEW_BUDGET_USD', 2),
|
|
546
|
+
ciReviewMaxTurns: intFromEnv('CI_REVIEW_MAX_TURNS', 25),
|
|
547
|
+
// AUTOMATIC CI reviews per workspace per UTC day. Not shared with the PR or ticket review caps.
|
|
548
|
+
// Manual runs never count. Default = shared CI_REVIEW_DAILY_CAP.
|
|
549
|
+
ciReviewDailyCap: intFromEnv('CI_REVIEW_DAILY_CAP', 20),
|
|
542
550
|
// ---- Diff-size cap ----
|
|
543
551
|
// A very large inlined diff is the dominant cost on a big PR (it's the cached
|
|
544
552
|
// prefix re-read every turn). The diff shown IN THE PROMPT is truncated at a
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { inArray } from 'drizzle-orm';
|
|
2
|
+
import { schema } from './client.js';
|
|
3
|
+
// THE CI-REVIEW HALF OF BOTH PR DELETE PATHS (deleteRepo in queries.ts, retention's
|
|
4
|
+
// deletePrSubtree). A CI review belongs to exactly one PR, so deleting the PR deletes its runs and
|
|
5
|
+
// their items. Children before parents, explicitly — the composite FKs cascade, but these paths run
|
|
6
|
+
// parent-last by hand on both dialects. Runs inside the caller's transaction.
|
|
7
|
+
export async function pruneCiReviewsForPrs(tx, prIds) {
|
|
8
|
+
if (prIds.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
const runs = schema.ciReviews;
|
|
11
|
+
const rows = await tx
|
|
12
|
+
.select({ id: runs.id })
|
|
13
|
+
.from(runs)
|
|
14
|
+
.where(inArray(runs.prId, [...prIds]))
|
|
15
|
+
.execute();
|
|
16
|
+
const runIds = rows.map((r) => r.id);
|
|
17
|
+
if (runIds.length > 0) {
|
|
18
|
+
await tx.delete(schema.ciReviewItems).where(inArray(schema.ciReviewItems.ciReviewId, runIds)).execute();
|
|
19
|
+
}
|
|
20
|
+
await tx.delete(runs).where(inArray(runs.prId, [...prIds])).execute();
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=ci-review-prune.js.map
|
package/dist/db/erase-account.js
CHANGED
|
@@ -128,6 +128,13 @@ export async function eraseAccountData(accountId) {
|
|
|
128
128
|
.delete(schema.ticketReviews)
|
|
129
129
|
.where(eq(schema.ticketReviews.accountId, accountId))
|
|
130
130
|
.execute();
|
|
131
|
+
// CI reviews (migration 0082 / pg 0069), items first. The repo loop has normally taken them with
|
|
132
|
+
// their PRs; explicit, like every agentic table.
|
|
133
|
+
await tx
|
|
134
|
+
.delete(schema.ciReviewItems)
|
|
135
|
+
.where(eq(schema.ciReviewItems.accountId, accountId))
|
|
136
|
+
.execute();
|
|
137
|
+
await tx.delete(schema.ciReviews).where(eq(schema.ciReviews.accountId, accountId)).execute();
|
|
131
138
|
// AI Fix runs (patches of the user's code + the prompts they were built from). Core since
|
|
132
139
|
// migration 0074 / pg 0061 — the plugin's erasure hook used to own them.
|
|
133
140
|
await tx.delete(schema.aiFixes).where(eq(schema.aiFixes.accountId, accountId)).execute();
|
|
@@ -211,6 +218,10 @@ export function accountScopedTables() {
|
|
|
211
218
|
col: schema.ticketReviewItems.accountId,
|
|
212
219
|
table: schema.ticketReviewItems,
|
|
213
220
|
},
|
|
221
|
+
// CI reviews (migration 0082 / pg 0069) — the runs and their per-check items. Both carry their
|
|
222
|
+
// own accountId; both are erased explicitly above.
|
|
223
|
+
{ name: 'ciReviews', col: schema.ciReviews.accountId, table: schema.ciReviews },
|
|
224
|
+
{ name: 'ciReviewItems', col: schema.ciReviewItems.accountId, table: schema.ciReviewItems },
|
|
214
225
|
{ name: 'aiUsage', col: aiUsage.accountId, table: aiUsage },
|
|
215
226
|
// Adopted from the plugin (migration 0074 / pg 0061) with AI Fix's fixer. No FKs, so nothing
|
|
216
227
|
// cascades: the explicit delete above is the whole guarantee. (`reviewLearnings`, review
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- CI REVIEW (local-only feature, like the rest of Claude Review): a separate Claude run that explains
|
|
2
|
+
-- why checks FAILED on a PR's head, split out of the PR review's own run. Two tables:
|
|
3
|
+
-- ci_reviews one row per run (history kept). Keyed by (pr, head, sorted failing check names):
|
|
4
|
+
-- `failing_key` is what the run read live, `trigger_key` the synced set that
|
|
5
|
+
-- started it.
|
|
6
|
+
-- ci_review_items one row per failing check: Claude's cause, or the server's not-checked reason.
|
|
7
|
+
-- Run ids arrive in request paths, so the item table's tenancy is STRUCTURAL: a composite FK against
|
|
8
|
+
-- ci_reviews(id, account_id), whose parent key `ci_reviews_id_account` is created here; the run's PR
|
|
9
|
+
-- against pull_requests(id, account_id). Both delete paths (deleteRepo, retention's deletePrSubtree)
|
|
10
|
+
-- and eraseAccountData delete these rows explicitly. `claude_reviews.ci_failures` becomes LEGACY
|
|
11
|
+
-- (read-only); nothing here touches it. The Postgres twin is migrations-pg/0069_ci_reviews.sql.
|
|
12
|
+
CREATE TABLE IF NOT EXISTS `ci_reviews` (
|
|
13
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
14
|
+
`account_id` integer NOT NULL,
|
|
15
|
+
`workspace_id` integer NOT NULL,
|
|
16
|
+
`pr_id` integer NOT NULL,
|
|
17
|
+
`repo_id` integer NOT NULL,
|
|
18
|
+
`head_sha` text NOT NULL,
|
|
19
|
+
`failing_key` text,
|
|
20
|
+
`trigger_key` text,
|
|
21
|
+
`failing_checks` text,
|
|
22
|
+
`trigger` text DEFAULT 'manual' NOT NULL,
|
|
23
|
+
`status` text NOT NULL,
|
|
24
|
+
`model` text NOT NULL,
|
|
25
|
+
`cost_usd` real,
|
|
26
|
+
`input_tokens` integer,
|
|
27
|
+
`output_tokens` integer,
|
|
28
|
+
`num_turns` integer,
|
|
29
|
+
`error` text,
|
|
30
|
+
`refused` text,
|
|
31
|
+
`ci_state` text,
|
|
32
|
+
`summary` text,
|
|
33
|
+
`started_at` integer,
|
|
34
|
+
`completed_at` integer,
|
|
35
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
36
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
37
|
+
CONSTRAINT `cir_pr_account_fk` FOREIGN KEY (`pr_id`,`account_id`) REFERENCES `pull_requests`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
38
|
+
);
|
|
39
|
+
--> statement-breakpoint
|
|
40
|
+
CREATE INDEX IF NOT EXISTS `cir_account_pr_created_idx` ON `ci_reviews` (`account_id`,`pr_id`,`created_at`);
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE INDEX IF NOT EXISTS `cir_account_ws_created_idx` ON `ci_reviews` (`account_id`,`workspace_id`,`created_at`);
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
CREATE UNIQUE INDEX IF NOT EXISTS `ci_reviews_id_account` ON `ci_reviews` (`id`,`account_id`);
|
|
45
|
+
--> statement-breakpoint
|
|
46
|
+
CREATE TABLE IF NOT EXISTS `ci_review_items` (
|
|
47
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
48
|
+
`ci_review_id` integer NOT NULL,
|
|
49
|
+
`account_id` integer NOT NULL,
|
|
50
|
+
`ref` text,
|
|
51
|
+
`check_name` text NOT NULL,
|
|
52
|
+
`job_id` integer,
|
|
53
|
+
`step` text,
|
|
54
|
+
`url` text,
|
|
55
|
+
`sent` integer DEFAULT false NOT NULL,
|
|
56
|
+
`carried` integer DEFAULT false NOT NULL,
|
|
57
|
+
`status` text NOT NULL,
|
|
58
|
+
`not_checked_reason` text,
|
|
59
|
+
`cause` text,
|
|
60
|
+
`explanation` text,
|
|
61
|
+
`category` text,
|
|
62
|
+
`fixable_in_pr` integer,
|
|
63
|
+
`path` text,
|
|
64
|
+
`line` integer,
|
|
65
|
+
`suggestion` text,
|
|
66
|
+
`related_files` text,
|
|
67
|
+
`assessed_at_head` text NOT NULL,
|
|
68
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
69
|
+
FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
70
|
+
CONSTRAINT `ciri_review_account_fk` FOREIGN KEY (`ci_review_id`,`account_id`) REFERENCES `ci_reviews`(`id`,`account_id`) ON UPDATE no action ON DELETE cascade
|
|
71
|
+
);
|
|
72
|
+
--> statement-breakpoint
|
|
73
|
+
CREATE INDEX IF NOT EXISTS `ciri_review_idx` ON `ci_review_items` (`ci_review_id`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- AUTO AI FIX, PER WORKSPACE — `workspaces.auto_fix_enabled`. After a succeeded AUTO Claude review
|
|
2
|
+
-- of the reader's OWN pull request, Limn may prepare one review-seeded AI Fix (never pushed;
|
|
3
|
+
-- coding/ai-fix/auto-fix.ts `maybeStartAutoFix`). This is that step's switch, edited in Settings
|
|
4
|
+
-- beside auto review and written by `setWorkspaceAutoReview` (review/claude-review/auto-settings.ts).
|
|
5
|
+
-- ⚠ DEFAULT TRUE (1) and NOT NULL: auto fix ran unconditionally before this column existed, so
|
|
6
|
+
-- every existing workspace keeps today's behaviour until someone switches it off. It only matters
|
|
7
|
+
-- while auto review is on, because it only runs after an auto review. The Postgres twin is
|
|
8
|
+
-- migrations-pg/0070_workspace_auto_fix.sql.
|
|
9
|
+
ALTER TABLE `workspaces` ADD `auto_fix_enabled` integer DEFAULT 1 NOT NULL;
|
|
@@ -575,6 +575,20 @@
|
|
|
575
575
|
"when": 1790658000000,
|
|
576
576
|
"tag": "0081_review_request_requester",
|
|
577
577
|
"breakpoints": true
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"idx": 82,
|
|
581
|
+
"version": "6",
|
|
582
|
+
"when": 1790661600000,
|
|
583
|
+
"tag": "0082_ci_reviews",
|
|
584
|
+
"breakpoints": true
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
"idx": 83,
|
|
588
|
+
"version": "6",
|
|
589
|
+
"when": 1790665200000,
|
|
590
|
+
"tag": "0083_workspace_auto_fix",
|
|
591
|
+
"breakpoints": true
|
|
578
592
|
}
|
|
579
593
|
]
|
|
580
594
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Postgres twin of migrations/0082_ci_reviews.sql, where the contract lives. CI review is
|
|
2
|
+
-- local-only, so the tables exist in Postgres for schema parity. `job_id` is BIGINT: GitHub Actions
|
|
3
|
+
-- job ids have passed 2^31.
|
|
4
|
+
CREATE TABLE IF NOT EXISTS "ci_reviews" (
|
|
5
|
+
"id" serial PRIMARY KEY NOT NULL,
|
|
6
|
+
"account_id" integer NOT NULL,
|
|
7
|
+
"workspace_id" integer NOT NULL,
|
|
8
|
+
"pr_id" integer NOT NULL,
|
|
9
|
+
"repo_id" integer NOT NULL,
|
|
10
|
+
"head_sha" text NOT NULL,
|
|
11
|
+
"failing_key" text,
|
|
12
|
+
"trigger_key" text,
|
|
13
|
+
"failing_checks" jsonb,
|
|
14
|
+
"trigger" text DEFAULT 'manual' NOT NULL,
|
|
15
|
+
"status" text NOT NULL,
|
|
16
|
+
"model" text NOT NULL,
|
|
17
|
+
"cost_usd" double precision,
|
|
18
|
+
"input_tokens" integer,
|
|
19
|
+
"output_tokens" integer,
|
|
20
|
+
"num_turns" integer,
|
|
21
|
+
"error" text,
|
|
22
|
+
"refused" text,
|
|
23
|
+
"ci_state" jsonb,
|
|
24
|
+
"summary" text,
|
|
25
|
+
"started_at" timestamp with time zone,
|
|
26
|
+
"completed_at" timestamp with time zone,
|
|
27
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
28
|
+
CONSTRAINT "ci_reviews_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE cascade,
|
|
29
|
+
CONSTRAINT "cir_pr_account_fk" FOREIGN KEY ("pr_id","account_id") REFERENCES "pull_requests"("id","account_id") ON DELETE cascade
|
|
30
|
+
);
|
|
31
|
+
--> statement-breakpoint
|
|
32
|
+
CREATE INDEX IF NOT EXISTS "cir_account_pr_created_idx" ON "ci_reviews" USING btree ("account_id","pr_id","created_at");
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE INDEX IF NOT EXISTS "cir_account_ws_created_idx" ON "ci_reviews" USING btree ("account_id","workspace_id","created_at");
|
|
35
|
+
--> statement-breakpoint
|
|
36
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "ci_reviews_id_account" ON "ci_reviews" USING btree ("id","account_id");
|
|
37
|
+
--> statement-breakpoint
|
|
38
|
+
CREATE TABLE IF NOT EXISTS "ci_review_items" (
|
|
39
|
+
"id" serial PRIMARY KEY NOT NULL,
|
|
40
|
+
"ci_review_id" integer NOT NULL,
|
|
41
|
+
"account_id" integer NOT NULL,
|
|
42
|
+
"ref" text,
|
|
43
|
+
"check_name" text NOT NULL,
|
|
44
|
+
"job_id" bigint,
|
|
45
|
+
"step" text,
|
|
46
|
+
"url" text,
|
|
47
|
+
"sent" boolean DEFAULT false NOT NULL,
|
|
48
|
+
"carried" boolean DEFAULT false NOT NULL,
|
|
49
|
+
"status" text NOT NULL,
|
|
50
|
+
"not_checked_reason" text,
|
|
51
|
+
"cause" text,
|
|
52
|
+
"explanation" text,
|
|
53
|
+
"category" text,
|
|
54
|
+
"fixable_in_pr" boolean,
|
|
55
|
+
"path" text,
|
|
56
|
+
"line" integer,
|
|
57
|
+
"suggestion" text,
|
|
58
|
+
"related_files" jsonb,
|
|
59
|
+
"assessed_at_head" text NOT NULL,
|
|
60
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
61
|
+
CONSTRAINT "ci_review_items_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE cascade,
|
|
62
|
+
CONSTRAINT "ciri_review_account_fk" FOREIGN KEY ("ci_review_id","account_id") REFERENCES "ci_reviews"("id","account_id") ON DELETE cascade
|
|
63
|
+
);
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
CREATE INDEX IF NOT EXISTS "ciri_review_idx" ON "ci_review_items" USING btree ("ci_review_id");
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
-- Postgres twin of migrations/0083_workspace_auto_fix.sql — the per-workspace auto AI Fix switch.
|
|
2
|
+
-- DEFAULT true so every existing workspace keeps today's behaviour. See the sqlite original.
|
|
3
|
+
ALTER TABLE "workspaces" ADD COLUMN IF NOT EXISTS "auto_fix_enabled" boolean DEFAULT true NOT NULL;
|
|
@@ -484,6 +484,20 @@
|
|
|
484
484
|
"when": 1790658000000,
|
|
485
485
|
"tag": "0068_review_request_requester",
|
|
486
486
|
"breakpoints": true
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"idx": 69,
|
|
490
|
+
"version": "7",
|
|
491
|
+
"when": 1790661600000,
|
|
492
|
+
"tag": "0069_ci_reviews",
|
|
493
|
+
"breakpoints": true
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"idx": 70,
|
|
497
|
+
"version": "7",
|
|
498
|
+
"when": 1790665200000,
|
|
499
|
+
"tag": "0070_workspace_auto_fix",
|
|
500
|
+
"breakpoints": true
|
|
487
501
|
}
|
|
488
502
|
]
|
|
489
503
|
}
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
// clock scores higher and wins `compareScored`'s age tie-break). Ties beyond that break on the
|
|
31
31
|
// item's own id, so two reads of unchanged data keep the same card.
|
|
32
32
|
//
|
|
33
|
+
// ⚠ A CLAUDE REVIEW IS EXEMPT: it never competes for the PR's one card and is never removed by
|
|
34
|
+
// it. A finished run, and above all an auto RE-review on a PR that already holds another job (a
|
|
35
|
+
// reply in your thread), is new work of its own; folding it under that job hid every re-review on
|
|
36
|
+
// a busy PR. Each run is its own item (its own id and `finishedAt` clock), so a newer run shows
|
|
37
|
+
// even after an older one's card was dismissed.
|
|
38
|
+
//
|
|
33
39
|
// ⚠ IT IS A BOARD RULE, NOT A NOTIFICATION RULE. `getWorkspaceInsights` asks for it; the
|
|
34
40
|
// account-wide `GET /api/my-turn` does not, because the browser-notification watcher diffs item
|
|
35
41
|
// ids — a deduplicated list would flip the winner whenever the top job cleared and announce the
|
|
@@ -285,14 +291,6 @@ export function onePerPr(mt, order) {
|
|
|
285
291
|
ref: `${i.threadId}`,
|
|
286
292
|
}),
|
|
287
293
|
};
|
|
288
|
-
const claude = {
|
|
289
|
-
of: (i) => ({
|
|
290
|
-
prId: i.prId,
|
|
291
|
-
reason: 'claude_review',
|
|
292
|
-
clockMs: clockMsOf(i.finishedAt),
|
|
293
|
-
ref: `${i.reviewId}`,
|
|
294
|
-
}),
|
|
295
|
-
};
|
|
296
294
|
for (const i of mt.awaitingReview)
|
|
297
295
|
offer(S.awaitingReview.of(i));
|
|
298
296
|
for (const i of mt.mentions)
|
|
@@ -319,8 +317,6 @@ export function onePerPr(mt, order) {
|
|
|
319
317
|
offer(threadReplies.of(i));
|
|
320
318
|
for (const i of mt.ownThreads)
|
|
321
319
|
offer(ownThreads.of(i));
|
|
322
|
-
for (const i of mt.claudeReviewsToAction)
|
|
323
|
-
offer(claude.of(i));
|
|
324
320
|
return {
|
|
325
321
|
awaitingReview: mt.awaitingReview.filter((i) => winnerIs(S.awaitingReview.of(i))),
|
|
326
322
|
mentions: mt.mentions.filter((i) => winnerIs(S.mentions.of(i))),
|
|
@@ -335,7 +331,8 @@ export function onePerPr(mt, order) {
|
|
|
335
331
|
threadsAwaiting: mt.threadsAwaiting.filter((i) => winnerIs(threadsAwaiting.of(i))),
|
|
336
332
|
threadReplies: mt.threadReplies.filter((i) => winnerIs(threadReplies.of(i))),
|
|
337
333
|
ownThreads: mt.ownThreads.filter((i) => winnerIs(ownThreads.of(i))),
|
|
338
|
-
|
|
334
|
+
// Exempt (header, rule 2): every Claude review keeps its own card.
|
|
335
|
+
claudeReviewsToAction: mt.claudeReviewsToAction,
|
|
339
336
|
// Repo-grained: a branch is not a pull request, and never shares a key with one.
|
|
340
337
|
redTrunks: mt.redTrunks,
|
|
341
338
|
};
|
package/dist/db/queries.js
CHANGED
|
@@ -50,6 +50,7 @@ import { forwardKindOf, isConflicting, isRedCiStatus, RED_CI_STATUSES, } from '.
|
|
|
50
50
|
import { authorAutomationFor, dependencyPrState, dependencyStateDetail, MERGE_QUEUE_CARD_DETAIL, securityFixDetail, unionAdvisoryIds, } from './dependency-cards.js';
|
|
51
51
|
import { deriveSecurityAlerts } from './security-alerts.js';
|
|
52
52
|
import { pruneTicketReviewsForPrs } from './ticket-review-prune.js';
|
|
53
|
+
import { pruneCiReviewsForPrs } from './ci-review-prune.js';
|
|
53
54
|
import { failingChecksFields, prFailingChecks, trunkFailingChecks, trunkFailingFor, } from './failing-checks.js';
|
|
54
55
|
import { addressedCommitAuthors, mentionExcerpts, reviewRequesters, threadFirstComments, yourPrNewActors, } from './my-turn-card-facts.js';
|
|
55
56
|
// Bind a JS Date into a raw-`sql` epoch comparison portably: Postgres columns are
|
|
@@ -8329,6 +8330,8 @@ export async function deleteRepo(id, accountId) {
|
|
|
8329
8330
|
// Ticket reviews (migration 0080 / pg 0067): this repo's member rows, then any run left with
|
|
8330
8331
|
// no members — a run whose other PRs live in another repo keeps its history.
|
|
8331
8332
|
await pruneTicketReviewsForPrs(tx, prIds);
|
|
8333
|
+
// CI reviews (migration 0082 / pg 0069): one PR each, so its runs and items go with it.
|
|
8334
|
+
await pruneCiReviewsForPrs(tx, prIds);
|
|
8332
8335
|
// The agentic table core ADOPTED from the plugin (migration 0074 / pg 0061): AI Fix runs
|
|
8333
8336
|
// (~MiB patches). No FKs, so nothing would cascade; before this the plugin had no deleteRepo
|
|
8334
8337
|
// hook at all and they were orphaned on a repo removal.
|
|
@@ -8561,19 +8564,21 @@ repoIds) {
|
|
|
8561
8564
|
// based: every tick it asks this read which PRs qualify, so the DATABASE is the queue and a restart
|
|
8562
8565
|
// loses nothing. A PR qualifies when it is, in ONE workspace:
|
|
8563
8566
|
// - OPEN and NOT A DRAFT (a draft qualifies once it is marked ready, if it was opened late enough);
|
|
8564
|
-
// - OPENED at or after `openedSinceMs` - the moment the workspace switched the
|
|
8565
|
-
// floor is what makes a first sync, a 90-day backfill or adding a repo safe:
|
|
8566
|
-
// make an OLD PR look new
|
|
8567
|
+
// - for a FIRST review: OPENED at or after `openedSinceMs` - the moment the workspace switched the
|
|
8568
|
+
// feature on. That floor is what makes a first sync, a 90-day backfill or adding a repo safe:
|
|
8569
|
+
// none of them can make an OLD PR look new. A RE-review also qualifies an older PR once it has a
|
|
8570
|
+
// succeeded run created at or after the floor (someone chose to review it since the switch);
|
|
8567
8571
|
// - by a PERSON. The workspace-aware resolver `hiddenBotUserIds` - the SAME set that decides
|
|
8568
8572
|
// `InsightPrRef.authorIsBot` and the "hide bots" lens, a manual workspace judgement winning both
|
|
8569
8573
|
// ways - never the global set alone. An unmapped author (`author_id` NULL) is not a proven
|
|
8570
8574
|
// person and is skipped;
|
|
8571
8575
|
// - with NO `claude_reviews` row at all, whatever its head, status or trigger: ONE review per PR,
|
|
8572
8576
|
// ever, from auto. A manual run (or a failed auto one) settles it too.
|
|
8573
|
-
// `
|
|
8574
|
-
//
|
|
8575
|
-
//
|
|
8576
|
-
//
|
|
8577
|
+
// Each re-review carries `lastRunAtMs` (the latest start or finish of any run of the PR) for the
|
|
8578
|
+
// sweeper's "start a new head on receipt unless a run was within 5 minutes" test, and
|
|
8579
|
+
// `inFlightMoved` lists PRs whose run is queued or running while their head moved past it (never
|
|
8580
|
+
// cancelled; the sweeper only starts that head's settle clock). There is NO CI reading: auto review
|
|
8581
|
+
// does not wait for CI (review/claude-review/auto.ts).
|
|
8577
8582
|
// `autoToday` counts this workspace's AUTO runs created at or after `dayStartMs`, for the daily cap.
|
|
8578
8583
|
// Returns null when the workspace is not this account's - never another workspace's answer (the
|
|
8579
8584
|
// request-side resolver's "fall back to Default" would review the wrong repos here).
|
|
@@ -8588,7 +8593,7 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8588
8593
|
return null;
|
|
8589
8594
|
const repoIds = await getWorkspaceRepoIds(workspaceId, accountId);
|
|
8590
8595
|
if (repoIds.length === 0)
|
|
8591
|
-
return { prIds: [], autoToday: 0, reReview: [],
|
|
8596
|
+
return { prIds: [], autoToday: 0, reReview: [], inFlightMoved: [] };
|
|
8592
8597
|
const autoRows = await db
|
|
8593
8598
|
.select({ id: claudeReviews.id })
|
|
8594
8599
|
.from(claudeReviews)
|
|
@@ -8597,21 +8602,20 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8597
8602
|
.execute();
|
|
8598
8603
|
const autoToday = autoRows.length;
|
|
8599
8604
|
if (opts.limit <= 0)
|
|
8600
|
-
return { prIds: [], autoToday, reReview: [],
|
|
8605
|
+
return { prIds: [], autoToday, reReview: [], inFlightMoved: [] };
|
|
8601
8606
|
const open = await db
|
|
8602
8607
|
.select({
|
|
8603
8608
|
id: pullRequests.id,
|
|
8604
8609
|
authorId: pullRequests.authorId,
|
|
8605
8610
|
openedAt: pullRequests.openedAt,
|
|
8606
8611
|
headSha: pullRequests.headSha,
|
|
8607
|
-
ciStatus: pullRequests.ciStatus,
|
|
8608
8612
|
})
|
|
8609
8613
|
.from(pullRequests)
|
|
8610
|
-
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, repoIds), eq(pullRequests.state, 'open'), eq(pullRequests.isDraft, false)
|
|
8614
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, repoIds), eq(pullRequests.state, 'open'), eq(pullRequests.isDraft, false)))
|
|
8611
8615
|
.orderBy(asc(pullRequests.openedAt), asc(pullRequests.id))
|
|
8612
8616
|
.execute();
|
|
8613
8617
|
if (open.length === 0)
|
|
8614
|
-
return { prIds: [], autoToday, reReview: [],
|
|
8618
|
+
return { prIds: [], autoToday, reReview: [], inFlightMoved: [] };
|
|
8615
8619
|
const [bots, reviewed, reviewerBots] = await Promise.all([
|
|
8616
8620
|
hiddenBotUserIds(accountId, workspaceId),
|
|
8617
8621
|
db
|
|
@@ -8621,6 +8625,7 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8621
8625
|
headSha: claudeReviews.headSha,
|
|
8622
8626
|
status: claudeReviews.status,
|
|
8623
8627
|
createdAt: claudeReviews.createdAt,
|
|
8628
|
+
finishedAt: claudeReviews.finishedAt,
|
|
8624
8629
|
commentsThrough: claudeReviews.commentsThrough,
|
|
8625
8630
|
})
|
|
8626
8631
|
.from(claudeReviews)
|
|
@@ -8631,8 +8636,13 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8631
8636
|
const botSet = new Set(bots);
|
|
8632
8637
|
const hasRun = new Set(reviewed.map((r) => r.prId));
|
|
8633
8638
|
const human = open.filter((r) => r.authorId != null && !botSet.has(r.authorId));
|
|
8639
|
+
// ⚠ THE ONBOARDING FLOOR GATES FIRST REVIEWS ONLY. A PR opened before the switch is never
|
|
8640
|
+
// reviewed unasked (switching on must not bill a backlog), but once someone has run a review on
|
|
8641
|
+
// it SINCE the switch, it is followed like any other: a manual run on an older PR used to leave
|
|
8642
|
+
// its later pushes un-re-reviewed for ever, because the floor filtered the whole candidate read.
|
|
8643
|
+
const afterFloor = (r) => r.openedAt != null && r.openedAt.getTime() >= opts.openedSinceMs;
|
|
8634
8644
|
const prIds = human
|
|
8635
|
-
.filter((r) => !hasRun.has(r.id))
|
|
8645
|
+
.filter((r) => afterFloor(r) && !hasRun.has(r.id))
|
|
8636
8646
|
.slice(0, opts.limit)
|
|
8637
8647
|
.map((r) => r.id);
|
|
8638
8648
|
// RE-REVIEW: a PR already reviewed (a SUCCEEDED run, manual or auto) whose synced head has
|
|
@@ -8646,20 +8656,31 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8646
8656
|
runsByPr.set(r.prId, list);
|
|
8647
8657
|
}
|
|
8648
8658
|
const reReview = [];
|
|
8659
|
+
const inFlightMoved = [];
|
|
8649
8660
|
// Same head, but maybe NEW REVIEW COMMENTS: the PR's runs at its head and how far they saw.
|
|
8650
8661
|
const sameHead = [];
|
|
8651
8662
|
for (const pr of human) {
|
|
8652
8663
|
const runs = runsByPr.get(pr.id);
|
|
8653
8664
|
if (!runs || !pr.headSha)
|
|
8654
8665
|
continue;
|
|
8666
|
+
const newest = runs.reduce((a, b) => (b.id > a.id ? b : a));
|
|
8667
|
+
if ((newest.status === 'queued' || newest.status === 'running') &&
|
|
8668
|
+
!runs.some((r) => r.headSha === pr.headSha)) {
|
|
8669
|
+
inFlightMoved.push({ prId: pr.id, headSha: pr.headSha });
|
|
8670
|
+
continue;
|
|
8671
|
+
}
|
|
8655
8672
|
if (!runs.some((r) => r.status === 'succeeded'))
|
|
8656
8673
|
continue;
|
|
8674
|
+
if (!afterFloor(pr) &&
|
|
8675
|
+
!runs.some((r) => r.status === 'succeeded' && r.createdAt.getTime() >= opts.openedSinceMs))
|
|
8676
|
+
continue;
|
|
8657
8677
|
const latest = runs.reduce((a, b) => (b.id > a.id ? b : a));
|
|
8658
8678
|
if (latest.status === 'queued' || latest.status === 'running')
|
|
8659
8679
|
continue;
|
|
8660
8680
|
const atHead = runs.filter((r) => r.headSha === pr.headSha);
|
|
8661
8681
|
if (atHead.length === 0) {
|
|
8662
|
-
|
|
8682
|
+
const lastRunAtMs = Math.max(...runs.map((r) => Math.max(r.createdAt.getTime(), r.finishedAt?.getTime() ?? 0)));
|
|
8683
|
+
reReview.push({ prId: pr.id, headSha: pr.headSha, commentsAtMs: null, reason: 'head', lastRunAtMs });
|
|
8663
8684
|
if (reReview.length >= opts.limit)
|
|
8664
8685
|
break;
|
|
8665
8686
|
continue;
|
|
@@ -8683,41 +8704,18 @@ export async function getAutoReviewCandidates(accountId, workspaceId, opts) {
|
|
|
8683
8704
|
const at = newest.get(p.prId);
|
|
8684
8705
|
if (!at || at.getTime() <= p.coveredMs)
|
|
8685
8706
|
continue;
|
|
8686
|
-
reReview.push({
|
|
8707
|
+
reReview.push({
|
|
8708
|
+
prId: p.prId,
|
|
8709
|
+
headSha: p.headSha,
|
|
8710
|
+
commentsAtMs: at.getTime(),
|
|
8711
|
+
reason: 'comments',
|
|
8712
|
+
lastRunAtMs: null,
|
|
8713
|
+
});
|
|
8687
8714
|
if (reReview.length >= opts.limit)
|
|
8688
8715
|
break;
|
|
8689
8716
|
}
|
|
8690
8717
|
}
|
|
8691
|
-
|
|
8692
|
-
return { prIds, autoToday, reReview, ci };
|
|
8693
|
-
}
|
|
8694
|
-
async function autoReviewCiStates(accountId, open, offered) {
|
|
8695
|
-
const rows = open.filter((r) => offered.has(r.id));
|
|
8696
|
-
if (rows.length === 0)
|
|
8697
|
-
return [];
|
|
8698
|
-
const events = (await db
|
|
8699
|
-
.select({
|
|
8700
|
-
prId: ciStatusEvents.prId,
|
|
8701
|
-
headSha: ciStatusEvents.headSha,
|
|
8702
|
-
observedAt: ciStatusEvents.observedAt,
|
|
8703
|
-
})
|
|
8704
|
-
.from(ciStatusEvents)
|
|
8705
|
-
.where(and(eq(ciStatusEvents.accountId, accountId), inArray(ciStatusEvents.prId, rows.map((r) => r.id))))
|
|
8706
|
-
.execute());
|
|
8707
|
-
const firstSeen = new Map();
|
|
8708
|
-
for (const e of events) {
|
|
8709
|
-
const k = `${e.prId}|${e.headSha}`;
|
|
8710
|
-
const t = e.observedAt.getTime();
|
|
8711
|
-
const prev = firstSeen.get(k);
|
|
8712
|
-
if (prev == null || t < prev)
|
|
8713
|
-
firstSeen.set(k, t);
|
|
8714
|
-
}
|
|
8715
|
-
return rows.map((r) => ({
|
|
8716
|
-
prId: r.id,
|
|
8717
|
-
headSha: r.headSha,
|
|
8718
|
-
running: r.ciStatus === 'pending' || r.ciStatus === 'expected',
|
|
8719
|
-
headSeenAtMs: r.headSha ? (firstSeen.get(`${r.id}|${r.headSha}`) ?? null) : null,
|
|
8720
|
-
}));
|
|
8718
|
+
return { prIds, autoToday, reReview, inFlightMoved };
|
|
8721
8719
|
}
|
|
8722
8720
|
// WHO MAY RE-TRIGGER AN AUTO REVIEW BY COMMENTING (the comment half of the re-review key). A
|
|
8723
8721
|
// PERSON — not in the workspace's bot union `hiddenBotUserIds`, where a manual "this is a human"
|