limn-review 0.1.140 → 0.1.142
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/coding/ai-fix/manager.js +4 -2
- package/dist/db/erase-account.js +4 -0
- package/dist/db/migrations/0137_review_pair_consolidation.sql +8 -0
- package/dist/db/migrations/0138_team_claim_lines.sql +55 -0
- package/dist/db/migrations/meta/_journal.json +14 -0
- package/dist/db/queries.js +34 -4
- package/dist/db/retention.js +4 -0
- package/dist/db/schema.sqlite.js +59 -0
- package/dist/db/team-claim-lines-delete.js +18 -0
- package/dist/platform/slack/signals.js +27 -3
- package/dist/review/agent-context.js +1 -0
- package/dist/review/agentic.js +5 -0
- package/dist/review/claude-review/auto-post.js +40 -31
- package/dist/review/claude-review/auto-verdict.js +17 -0
- package/dist/review/claude-review/auto.js +5 -0
- package/dist/review/claude-review/chat.js +76 -14
- package/dist/review/claude-review/finding-facts.js +27 -0
- package/dist/review/claude-review/manager.js +78 -12
- package/dist/review/claude-review/manual-reply.js +3 -2
- package/dist/review/claude-review/pair-consolidate.js +302 -0
- package/dist/review/claude-review/pair-view.js +248 -0
- package/dist/review/claude-review/pair.js +7 -3
- package/dist/review/claude-review/persist.js +139 -36
- package/dist/review/claude-review/routes.js +70 -128
- package/dist/review/claude-review/team-claim-ledger.js +135 -0
- package/dist/review/claude-review/team-claim-release.js +274 -0
- package/dist/review/claude-review/team-claim.js +200 -34
- package/dist/review/llm.js +1 -1
- package/dist/shared/ai-provider-words.js +28 -1
- package/package.json +1 -1
- package/public/assets/AiFixTab-B3eAgc7u.js +3 -0
- package/public/assets/ClaudeReviewTab-C7GoZdiF.js +20 -0
- package/public/assets/{PrBotBehaviourTab-Bwu839bx.js → PrBotBehaviourTab-ChRG19sQ.js} +1 -1
- package/public/assets/{index-BoS_VHWj.js → index-BvK15F-9.js} +46 -46
- package/public/assets/{index-B23U_yh7.css → index-CY_rIeEn.css} +1 -1
- package/public/index.html +2 -2
- package/public/assets/AiFixTab-D8HrsNZV.js +0 -3
- package/public/assets/ClaudeReviewTab-CJpvYk_E.js +0 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, desc, eq } from 'drizzle-orm';
|
|
2
2
|
import { AI_FIX_PLAN_MAX_REVISIONS, aiRunProvider } from '../../shared/index.js';
|
|
3
3
|
import { getFixPrContext } from './pr-context.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getReviewView } from '../../review/claude-review/persist.js';
|
|
5
5
|
import { currentThreadAssessmentsForPrs } from '../../review/claude-review/thread-judgements.js';
|
|
6
6
|
import { pickReviewNonce } from '../../review/claude-review/prompts.js';
|
|
7
7
|
import { buildPickerPreview, buildReviewSeed, } from './review-seed.js';
|
|
@@ -103,7 +103,9 @@ function emitFixStream(prId, e) {
|
|
|
103
103
|
async function loadSourceReview(ctx, accountId, prId, reviewId) {
|
|
104
104
|
if (reviewId == null)
|
|
105
105
|
return null;
|
|
106
|
-
|
|
106
|
+
// A Claude + Codex pair seeds from its ONE consolidated review (both halves' findings, merged-away
|
|
107
|
+
// duplicates never read — pair-view.ts), never from one half.
|
|
108
|
+
const review = await getReviewView(ctx, reviewId, accountId);
|
|
107
109
|
if (!review || review.prId !== prId || review.status !== 'succeeded')
|
|
108
110
|
return null;
|
|
109
111
|
// A later Check threads run judged threads since this review: when this IS the latest review,
|
package/dist/db/erase-account.js
CHANGED
|
@@ -168,6 +168,8 @@ export async function eraseAccountData(accountId) {
|
|
|
168
168
|
// hold third-party analysis of the account's code. No FK on the snapshots, so this is the whole
|
|
169
169
|
// guarantee; workspace_sonar cascades from workspaces too, and is explicit anyway.
|
|
170
170
|
await tx.delete(schema.sonarSnapshots).where(eq(schema.sonarSnapshots.accountId, accountId)).execute();
|
|
171
|
+
// The team-claim ledger (migration 0138): GitHub logins and heads. No FK, so this is the whole guarantee.
|
|
172
|
+
await tx.delete(schema.teamClaimLines).where(eq(schema.teamClaimLines.accountId, accountId)).execute();
|
|
171
173
|
await tx.delete(schema.workspaceSonar).where(eq(schema.workspaceSonar.accountId, accountId)).execute();
|
|
172
174
|
// Sequence diagrams (migration 0115 / pg 0102): no FKs, so this is the whole guarantee (story rows
|
|
173
175
|
// carry no pr_id and are reached ONLY here).
|
|
@@ -330,6 +332,8 @@ export function accountScopedTables() {
|
|
|
330
332
|
{ name: 'workspaceTrackers', col: schema.workspaceTrackers.accountId, table: schema.workspaceTrackers },
|
|
331
333
|
// SonarCloud (migration 0114 / pg 0101), erased explicitly above; `workspaceSonar` holds a credential.
|
|
332
334
|
{ name: 'sonarSnapshots', col: schema.sonarSnapshots.accountId, table: schema.sonarSnapshots },
|
|
335
|
+
// The team-claim ledger (migration 0138), erased explicitly above.
|
|
336
|
+
{ name: 'teamClaimLines', col: schema.teamClaimLines.accountId, table: schema.teamClaimLines },
|
|
333
337
|
{ name: 'workspaceSonar', col: schema.workspaceSonar.accountId, table: schema.workspaceSonar },
|
|
334
338
|
// Sequence diagrams (migration 0115 / pg 0102), erased explicitly above.
|
|
335
339
|
{ name: 'sequenceDiagrams', col: schema.sequenceDiagrams.accountId, table: schema.sequenceDiagrams },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- CLAUDE + CODEX: ONE REVIEW BY BOTH (docs/CLAUDE-REVIEW.md § Claude + Codex). When a pair ends its two
|
|
2
|
+
-- halves are consolidated into one review. `pair_consolidation` (on the pair's FIRST row only) holds
|
|
3
|
+
-- the one summary, the code-computed verdict, each kept finding's source, the merged-away duplicates
|
|
4
|
+
-- and the half that did not finish; NULL everywhere else. `merged_into` marks a finding the other
|
|
5
|
+
-- half's finding stands for (never shown, posted, counted or sent to AI Fix); NULL everywhere else.
|
|
6
|
+
ALTER TABLE `claude_reviews` ADD `pair_consolidation` text;
|
|
7
|
+
--> statement-breakpoint
|
|
8
|
+
ALTER TABLE `claude_review_findings` ADD `merged_into` integer;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- THE TEAM-CLAIM LEDGER (docs/CLAUDE-REVIEW.md § Team claims → Releasing stale lines). One row per
|
|
2
|
+
-- GitHub status line (`limn/review-claude` | `limn/review-codex`) Limn holds or released on a PR
|
|
3
|
+
-- head, written BEFORE the pending line is posted, so a restart, a dropped run or a failed final write
|
|
4
|
+
-- never leaves this machine's line pending: the reconciler releases every open row whose run is not
|
|
5
|
+
-- running. `whose = 'teammate'` records a teammate's expired line released once.
|
|
6
|
+
CREATE TABLE `team_claim_lines` (
|
|
7
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
8
|
+
`account_id` integer NOT NULL,
|
|
9
|
+
`pr_id` integer NOT NULL,
|
|
10
|
+
`repo_id` integer,
|
|
11
|
+
`owner` text,
|
|
12
|
+
`name` text,
|
|
13
|
+
`head_sha` text NOT NULL,
|
|
14
|
+
`provider` text NOT NULL,
|
|
15
|
+
`review_id` integer,
|
|
16
|
+
`whose` text DEFAULT 'mine' NOT NULL,
|
|
17
|
+
`state` text NOT NULL,
|
|
18
|
+
`login` text,
|
|
19
|
+
`final_description` text,
|
|
20
|
+
`final_target_url` text,
|
|
21
|
+
`release_attempts` integer DEFAULT 0 NOT NULL,
|
|
22
|
+
`last_error` text,
|
|
23
|
+
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
|
24
|
+
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
|
25
|
+
);
|
|
26
|
+
--> statement-breakpoint
|
|
27
|
+
CREATE INDEX `team_claim_lines_state` ON `team_claim_lines` (`state`);
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
CREATE INDEX `team_claim_lines_account_pr_head` ON `team_claim_lines` (`account_id`,`pr_id`,`head_sha`);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
-- Lines older Limn versions left PENDING (a failed run's "didn't finish · free to claim", or a run that
|
|
32
|
+
-- died mid-way): the newest auto run's record per (PR, head, context), when it is still pending, from
|
|
33
|
+
-- the last 14 days. The reconciler replaces each with the success release line at boot — only where the
|
|
34
|
+
-- head's newest line is still this account's pending one (it reads GitHub first).
|
|
35
|
+
INSERT INTO `team_claim_lines` (`account_id`, `pr_id`, `repo_id`, `owner`, `name`, `head_sha`, `provider`, `review_id`, `whose`, `state`)
|
|
36
|
+
SELECT cr.`account_id`, cr.`pr_id`, pr.`repo_id`, r.`owner`, r.`name`, cr.`head_sha`,
|
|
37
|
+
CASE json_extract(cr.`team_claim`, '$.context') WHEN 'limn/review-codex' THEN 'codex' ELSE 'claude' END,
|
|
38
|
+
cr.`id`, 'mine', 'pending'
|
|
39
|
+
FROM `claude_reviews` cr
|
|
40
|
+
JOIN `pull_requests` pr ON pr.`id` = cr.`pr_id`
|
|
41
|
+
JOIN `repos` r ON r.`id` = pr.`repo_id`
|
|
42
|
+
WHERE cr.`team_claim` IS NOT NULL
|
|
43
|
+
AND json_valid(cr.`team_claim`)
|
|
44
|
+
AND json_extract(cr.`team_claim`, '$.state') = 'marked'
|
|
45
|
+
AND json_extract(cr.`team_claim`, '$.lastLine') IN ('reviewing', 'unfinished')
|
|
46
|
+
AND cr.`created_at` >= unixepoch() - 14 * 86400
|
|
47
|
+
AND cr.`id` = (
|
|
48
|
+
SELECT MAX(c2.`id`) FROM `claude_reviews` c2
|
|
49
|
+
WHERE c2.`account_id` = cr.`account_id`
|
|
50
|
+
AND c2.`pr_id` = cr.`pr_id`
|
|
51
|
+
AND c2.`head_sha` = cr.`head_sha`
|
|
52
|
+
AND c2.`team_claim` IS NOT NULL
|
|
53
|
+
AND json_valid(c2.`team_claim`)
|
|
54
|
+
AND json_extract(c2.`team_claim`, '$.context') = json_extract(cr.`team_claim`, '$.context')
|
|
55
|
+
);
|
|
@@ -960,6 +960,20 @@
|
|
|
960
960
|
"when": 1792137623016,
|
|
961
961
|
"tag": "0136_review_kind",
|
|
962
962
|
"breakpoints": true
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
"idx": 137,
|
|
966
|
+
"version": "6",
|
|
967
|
+
"when": 1792137624016,
|
|
968
|
+
"tag": "0137_review_pair_consolidation",
|
|
969
|
+
"breakpoints": true
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
"idx": 138,
|
|
973
|
+
"version": "6",
|
|
974
|
+
"when": 1792137625016,
|
|
975
|
+
"tag": "0138_team_claim_lines",
|
|
976
|
+
"breakpoints": true
|
|
963
977
|
}
|
|
964
978
|
]
|
|
965
979
|
}
|
package/dist/db/queries.js
CHANGED
|
@@ -18,6 +18,7 @@ const REASON_PRIORITY = [
|
|
|
18
18
|
];
|
|
19
19
|
import { db, schema } from './client.js';
|
|
20
20
|
import { dropRepoRelations } from './related-repos.js';
|
|
21
|
+
import { deleteSettledClaimLinesForPrs } from './team-claim-lines-delete.js';
|
|
21
22
|
import { runTransaction } from './client.js';
|
|
22
23
|
import { enrichMyTurn } from '../feed/my-turn.js';
|
|
23
24
|
import { resolvePrTickets } from '../pr/detail-enricher.js';
|
|
@@ -7873,6 +7874,9 @@ export async function deleteRepo(id, accountId) {
|
|
|
7873
7874
|
await tx.delete(schema.trackerTickets).where(inArray(schema.trackerTickets.prId, prIds)).execute();
|
|
7874
7875
|
// SonarCloud snapshots (migration 0114 / pg 0101). No FKs, so nothing would cascade.
|
|
7875
7876
|
await tx.delete(schema.sonarSnapshots).where(inArray(schema.sonarSnapshots.prId, prIds)).execute();
|
|
7877
|
+
// The team-claim ledger (migration 0138). No FKs, so nothing would cascade. This machine's OPEN
|
|
7878
|
+
// rows stay: their lines still need releasing on GitHub (db/team-claim-lines-delete.ts).
|
|
7879
|
+
await deleteSettledClaimLinesForPrs(tx, prIds);
|
|
7876
7880
|
// Sequence diagrams of these PRs, and the story diagrams naming any of them (migration 0115 /
|
|
7877
7881
|
// pg 0102). No FKs, so nothing would cascade. Before the PRs go: it reads their accounts.
|
|
7878
7882
|
await pruneSequenceDiagramsForPrs(tx, prIds);
|
|
@@ -8038,6 +8042,7 @@ repoIds) {
|
|
|
8038
8042
|
reviewHead: claudeReviews.headSha,
|
|
8039
8043
|
prHead: pullRequests.headSha,
|
|
8040
8044
|
trigger: claudeReviews.trigger,
|
|
8045
|
+
pairId: claudeReviews.pairId,
|
|
8041
8046
|
})
|
|
8042
8047
|
.from(claudeReviews)
|
|
8043
8048
|
.innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
|
|
@@ -8049,16 +8054,41 @@ repoIds) {
|
|
|
8049
8054
|
.execute();
|
|
8050
8055
|
const seen = new Set(); // most-recent succeeded run per PR only
|
|
8051
8056
|
const candidates = [];
|
|
8057
|
+
// A CLAUDE + CODEX PAIR is ONE review (review/claude-review/pair-view.ts): its succeeded halves
|
|
8058
|
+
// are read together — posted when either was posted as a review, its findings both halves' (a
|
|
8059
|
+
// merged-away duplicate never counts), its verdict the pair's one verdict.
|
|
8060
|
+
const halvesOf = new Map();
|
|
8061
|
+
for (const r of rows) {
|
|
8062
|
+
if (r.pairId == null)
|
|
8063
|
+
continue;
|
|
8064
|
+
const list = halvesOf.get(r.pairId) ?? [];
|
|
8065
|
+
list.push(r);
|
|
8066
|
+
halvesOf.set(r.pairId, list);
|
|
8067
|
+
}
|
|
8052
8068
|
for (const r of rows) {
|
|
8053
8069
|
if (seen.has(r.prId))
|
|
8054
8070
|
continue;
|
|
8055
8071
|
seen.add(r.prId);
|
|
8056
|
-
|
|
8072
|
+
const halves = r.pairId != null ? (halvesOf.get(r.pairId) ?? [r]) : [r];
|
|
8073
|
+
if (halves.some((h) => h.postedAt != null))
|
|
8057
8074
|
continue; // the run itself was posted → actioned, no findings needed
|
|
8058
8075
|
candidates.push(r);
|
|
8059
8076
|
}
|
|
8060
8077
|
if (candidates.length === 0)
|
|
8061
8078
|
return [];
|
|
8079
|
+
const runIdsOf = (c) => c.pairId != null ? (halvesOf.get(c.pairId) ?? [c]).map((h) => h.reviewId) : [c.reviewId];
|
|
8080
|
+
// The pairs' one verdicts (on each pair's FIRST row), in ONE read.
|
|
8081
|
+
const pairIds = [...new Set(candidates.map((c) => c.pairId).filter((id) => id != null))];
|
|
8082
|
+
const pairVerdicts = new Map();
|
|
8083
|
+
if (pairIds.length > 0) {
|
|
8084
|
+
const recs = await db
|
|
8085
|
+
.select({ id: claudeReviews.id, rec: claudeReviews.pairConsolidation })
|
|
8086
|
+
.from(claudeReviews)
|
|
8087
|
+
.where(inArray(claudeReviews.id, pairIds))
|
|
8088
|
+
.execute();
|
|
8089
|
+
for (const p of recs)
|
|
8090
|
+
pairVerdicts.set(p.id, p.rec?.verdict ?? null);
|
|
8091
|
+
}
|
|
8062
8092
|
// ONE grouped read over the candidate runs — never one query per run, and never a per-run
|
|
8063
8093
|
// EXISTS in the select above, which would run the sub-query for every historical run of every
|
|
8064
8094
|
// open PR rather than the handful that got this far.
|
|
@@ -8070,7 +8100,7 @@ repoIds) {
|
|
|
8070
8100
|
postedAt: claudeReviewFindings.postedAt,
|
|
8071
8101
|
})
|
|
8072
8102
|
.from(claudeReviewFindings)
|
|
8073
|
-
.where(inArray(claudeReviewFindings.reviewId, candidates.
|
|
8103
|
+
.where(and(inArray(claudeReviewFindings.reviewId, candidates.flatMap(runIdsOf)), isNull(claudeReviewFindings.mergedInto)))
|
|
8074
8104
|
.execute();
|
|
8075
8105
|
const findingsByRun = new Map();
|
|
8076
8106
|
for (const f of findingRows) {
|
|
@@ -8080,7 +8110,7 @@ repoIds) {
|
|
|
8080
8110
|
}
|
|
8081
8111
|
const out = [];
|
|
8082
8112
|
for (const r of candidates) {
|
|
8083
|
-
const findings = findingsByRun.get(
|
|
8113
|
+
const findings = runIdsOf(r).flatMap((id) => findingsByRun.get(id) ?? []);
|
|
8084
8114
|
// STATE ONE: nothing from this run has reached GitHub. It was never worked, so it stays —
|
|
8085
8115
|
// including a run with no findings at all, whose summary and verdict are still yours to post
|
|
8086
8116
|
// or discard. ⚠ This branch is what keeps a fresh run visible: `included` defaults to false,
|
|
@@ -8101,7 +8131,7 @@ repoIds) {
|
|
|
8101
8131
|
repoFullName: `${r.owner}/${r.name}`,
|
|
8102
8132
|
prNumber: r.prNumber,
|
|
8103
8133
|
prTitle: r.prTitle,
|
|
8104
|
-
verdict: r.verdict,
|
|
8134
|
+
verdict: r.pairId != null ? (pairVerdicts.get(r.pairId) ?? r.verdict) : r.verdict,
|
|
8105
8135
|
finishedAt: iso(r.finishedAt),
|
|
8106
8136
|
headStale: r.reviewHead !== r.prHead,
|
|
8107
8137
|
githubUrl: `https://github.com/${r.owner}/${r.name}/pull/${r.prNumber}`,
|
package/dist/db/retention.js
CHANGED
|
@@ -4,6 +4,7 @@ import { config } from '../config.js';
|
|
|
4
4
|
import { pruneTicketReviewsForPrs } from './ticket-review-prune.js';
|
|
5
5
|
import { pruneCiReviewsForPrs } from './ci-review-prune.js';
|
|
6
6
|
import { pruneSequenceDiagramsForPrs } from './sequence-diagram-prune.js';
|
|
7
|
+
import { deleteSettledClaimLinesForPrs } from './team-claim-lines-delete.js';
|
|
7
8
|
// Time-based retention sweep. Per-account server data (events, PRs + their whole subtree)
|
|
8
9
|
// accumulates forever otherwise; this prunes anything past the retention window so the DB
|
|
9
10
|
// stays bounded (the pre-launch, tight-budget scalability lever). Runs periodically from
|
|
@@ -100,6 +101,9 @@ async function deletePrSubtree(tx, prIds) {
|
|
|
100
101
|
await tx.delete(schema.trackerTickets).where(inArray(schema.trackerTickets.prId, prIds)).execute();
|
|
101
102
|
// SonarCloud snapshots (migration 0114 / pg 0101). No FKs — explicit, pr-keyed.
|
|
102
103
|
await tx.delete(schema.sonarSnapshots).where(inArray(schema.sonarSnapshots.prId, prIds)).execute();
|
|
104
|
+
// The team-claim ledger (migration 0138). No FKs — explicit, pr-keyed. This machine's OPEN rows
|
|
105
|
+
// stay: their lines still need releasing on GitHub (db/team-claim-lines-delete.ts).
|
|
106
|
+
await deleteSettledClaimLinesForPrs(tx, prIds);
|
|
103
107
|
// Sequence diagrams of these PRs, and the story diagrams naming any of them (migration 0115 / pg
|
|
104
108
|
// 0102). No FKs. Before the PRs go: it reads their accounts.
|
|
105
109
|
await pruneSequenceDiagramsForPrs(tx, prIds);
|
package/dist/db/schema.sqlite.js
CHANGED
|
@@ -1113,6 +1113,12 @@ export const claudeReviews = sqliteTable('claude_reviews', {
|
|
|
1113
1113
|
pairId: integer('pair_id'),
|
|
1114
1114
|
pairRole: text('pair_role', { enum: ['first', 'second'] }),
|
|
1115
1115
|
pairDropped: integer('pair_dropped'),
|
|
1116
|
+
// THE PAIR'S ONE REVIEW (migration 0137; review/claude-review/pair-consolidate.ts): on the pair's
|
|
1117
|
+
// FIRST row only, written ONCE when the pair ends — the consolidated summary, the code verdict,
|
|
1118
|
+
// each kept finding's source, the merged-away duplicates and the half that did not finish.
|
|
1119
|
+
// NULL on every other row, on a pair still running, and on a pair from before the column (read as
|
|
1120
|
+
// the two halves joined in code).
|
|
1121
|
+
pairConsolidation: text('pair_consolidation', { mode: 'json' }).$type(),
|
|
1116
1122
|
// A REPLIES PASS (review/claude-review/replies-check.ts): 'replies' on a run that judged only
|
|
1117
1123
|
// people's replies on Limn's own posted findings; NULL on every full review (migration 0136).
|
|
1118
1124
|
// ⚠ Every "the latest review" / "a run at this head" reader filters `kind IS NULL`.
|
|
@@ -1207,6 +1213,12 @@ export const claudeReviewFindings = sqliteTable('claude_review_findings', {
|
|
|
1207
1213
|
// The reference-library sections this finding rests on, server-checked (migration 0117 / pg 0104).
|
|
1208
1214
|
// NULL = none.
|
|
1209
1215
|
docCitations: text('doc_citations', { mode: 'json' }).$type(),
|
|
1216
|
+
// A CLAUDE + CODEX PAIR'S DUPLICATE (migration 0137): the id of the finding kept in this one's
|
|
1217
|
+
// place when the pair was consolidated (the other half made the same point). A soft reference to
|
|
1218
|
+
// a finding of the same pair (no FK, like `prior_finding_id`). A merged-away finding stays here
|
|
1219
|
+
// but is NEVER shown, posted, counted in a verdict or sent to AI Fix; the next follow-up of its
|
|
1220
|
+
// provider follows up the kept one instead. NULL on every other finding.
|
|
1221
|
+
mergedInto: integer('merged_into'),
|
|
1210
1222
|
}, (t) => ({ reviewIdx: index('crf_review_idx').on(t.reviewId) }));
|
|
1211
1223
|
// ---- Claude Review chat (CORE table, written by the Pro plugin) ----
|
|
1212
1224
|
// One row per chat MESSAGE about one succeeded review: a general thread (`finding_id` NULL) and
|
|
@@ -3378,4 +3390,51 @@ export const storyDocsAlignments = sqliteTable('story_docs_alignments', {
|
|
|
3378
3390
|
foreignColumns: [workspaces.id, workspaces.accountId],
|
|
3379
3391
|
}).onDelete('cascade'),
|
|
3380
3392
|
}));
|
|
3393
|
+
// team_claim_lines — THE TEAM-CLAIM LEDGER (migration 0138; docs/CLAUDE-REVIEW.md § Team claims →
|
|
3394
|
+
// Releasing stale lines). ONE row per GitHub status line Limn holds or has released on a PR head, so a
|
|
3395
|
+
// line this machine posted is never left pending by a restart, a dropped run or a failed final write:
|
|
3396
|
+
// whose 'mine' a claim this machine made. Written as 'claiming' BEFORE the pending line is posted,
|
|
3397
|
+
// 'pending' once it is up, 'finished' when the success line ("reviewed") is up,
|
|
3398
|
+
// 'released' when the release line ("didn't finish · free to claim"), a Clear, a
|
|
3399
|
+
// newer claim at the same head, or GitHub's refusal ended it.
|
|
3400
|
+
// whose 'teammate' a teammate's expired line this machine released ("Released: bob's claim
|
|
3401
|
+
// expired"), recorded so it is posted ONCE. Always 'released'.
|
|
3402
|
+
// The reconciler (review/claude-review/team-claim-release.ts) releases every 'claiming' / 'pending'
|
|
3403
|
+
// row whose run is not queued or running in this process. No FK on pr_id (the sonar_snapshots
|
|
3404
|
+
// precedent): BOTH delete paths (deleteRepo, retention's deletePrSubtree) delete its rows EXCEPT this
|
|
3405
|
+
// machine's OPEN ones (a pending line on GitHub still needs its release; the row carries owner/name);
|
|
3406
|
+
// the erasure deletes everything.
|
|
3407
|
+
export const teamClaimLines = sqliteTable('team_claim_lines', {
|
|
3408
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
3409
|
+
accountId: integer('account_id').notNull(),
|
|
3410
|
+
prId: integer('pr_id').notNull(),
|
|
3411
|
+
repoId: integer('repo_id'),
|
|
3412
|
+
// The repo's owner/name when the row was written: an open line is still released after its PR or
|
|
3413
|
+
// repo left Limn (the delete paths keep OPEN 'mine' rows for exactly that).
|
|
3414
|
+
owner: text('owner'),
|
|
3415
|
+
name: text('name'),
|
|
3416
|
+
headSha: text('head_sha').notNull(),
|
|
3417
|
+
// 'claude' | 'codex' — the status context (`limn/review-<provider>`).
|
|
3418
|
+
provider: text('provider').notNull(),
|
|
3419
|
+
// The run the claim was for (NULL until its row is written, and on a teammate's line).
|
|
3420
|
+
reviewId: integer('review_id'),
|
|
3421
|
+
whose: text('whose', { enum: ['mine', 'teammate'] }).notNull().default('mine'),
|
|
3422
|
+
state: text('state', { enum: ['claiming', 'pending', 'finished', 'released'] }).notNull(),
|
|
3423
|
+
// The GitHub login the line names (NULL on a row backfilled from before the ledger: the
|
|
3424
|
+
// account's login is used).
|
|
3425
|
+
login: text('login'),
|
|
3426
|
+
// A success line that could not be written when the run finished: the reconciler posts IT
|
|
3427
|
+
// instead of the release line.
|
|
3428
|
+
finalDescription: text('final_description'),
|
|
3429
|
+
finalTargetUrl: text('final_target_url'),
|
|
3430
|
+
releaseAttempts: integer('release_attempts').notNull().default(0),
|
|
3431
|
+
lastError: text('last_error'),
|
|
3432
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
3433
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql `(unixepoch())`),
|
|
3434
|
+
}, (t) => ({
|
|
3435
|
+
// The reconciler's read (open rows).
|
|
3436
|
+
stateIdx: index('team_claim_lines_state').on(t.state),
|
|
3437
|
+
// One head's rows (supersede, Clear, the tab's "Clear" offer).
|
|
3438
|
+
prHeadIdx: index('team_claim_lines_account_pr_head').on(t.accountId, t.prId, t.headSha),
|
|
3439
|
+
}));
|
|
3381
3440
|
//# sourceMappingURL=schema.sqlite.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The team-claim ledger's DELETE-PATH rule (migration 0138; docs/CLAUDE-REVIEW.md § Team claims →
|
|
2
|
+
// Releasing stale lines). When PRs leave Limn (deleteRepo, retention's deletePrSubtree), their ledger
|
|
3
|
+
// rows go — EXCEPT this machine's OPEN ones ('mine', 'claiming' / 'pending'): each stands for a line
|
|
4
|
+
// that may still be pending on GitHub, and the reconciler releases it from the row's own owner/name
|
|
5
|
+
// (it closes a row that has neither). The account erasure deletes every row regardless.
|
|
6
|
+
import { and, eq, inArray, not } from 'drizzle-orm';
|
|
7
|
+
import { schema } from './client.js';
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
9
|
+
export async function deleteSettledClaimLinesForPrs(tx, prIds) {
|
|
10
|
+
if (prIds.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const tl = schema.teamClaimLines;
|
|
13
|
+
await tx
|
|
14
|
+
.delete(tl)
|
|
15
|
+
.where(and(inArray(tl.prId, prIds), not(and(eq(tl.whose, 'mine'), inArray(tl.state, ['claiming', 'pending'])))))
|
|
16
|
+
.execute();
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=team-claim-lines-delete.js.map
|
|
@@ -3,7 +3,7 @@ import { proSchemaFor } from '../db.js';
|
|
|
3
3
|
import { escapeSlack, prLink, slackLink } from './links.js';
|
|
4
4
|
import { postToSlack } from './send.js';
|
|
5
5
|
import { readTargetRow } from './targets.js';
|
|
6
|
-
import { aiRunProviderName } from '../../shared/index.js';
|
|
6
|
+
import { aiRunProviderName, PAIR_RUN_LABEL } from '../../shared/index.js';
|
|
7
7
|
// ── THE TWO SLACK EVENT SIGNALS (Pro, `slackDigest`; plugin migration 0040) ──────────────────
|
|
8
8
|
//
|
|
9
9
|
// Per workspace, each its own switch on the delivery row, both OFF by default:
|
|
@@ -105,7 +105,9 @@ const trimSummary = (s) => {
|
|
|
105
105
|
};
|
|
106
106
|
/** Pure builder for the "Claude Review ran" message ("Codex Review ran" when Codex ran it). Exported
|
|
107
107
|
* for its test. */
|
|
108
|
-
export function reviewMessage(ctx, workspaceId, f, verdict, summary,
|
|
108
|
+
export function reviewMessage(ctx, workspaceId, f, verdict, summary,
|
|
109
|
+
// 'Claude + Codex' for a pair's one review.
|
|
110
|
+
ranBy = 'Claude') {
|
|
109
111
|
const by = f.authorLogin ? ` by @${escapeSlack(f.authorLogin)}` : '';
|
|
110
112
|
const head = `*${ranBy} Review ran* on ${prLine(ctx, workspaceId, f)}${by}`;
|
|
111
113
|
const lines = [head];
|
|
@@ -161,6 +163,8 @@ export async function onClaudeReviewCompleted(ctx, e) {
|
|
|
161
163
|
summary: cr.summary,
|
|
162
164
|
reviewMode: cr.reviewMode,
|
|
163
165
|
trigger: cr.trigger,
|
|
166
|
+
// A Claude + Codex pair announces its ONE review (pair-view.ts): the consolidation on its first row.
|
|
167
|
+
...(cr.pairId != null ? { pairId: cr.pairId } : {}),
|
|
164
168
|
// The provider that ran it: the stored `provider` (Phase 4, sqlite 0128), else read from the
|
|
165
169
|
// model id (aiRunProvider). A schema without the columns reads as Claude.
|
|
166
170
|
...(cr.model != null ? { model: cr.model } : {}),
|
|
@@ -177,10 +181,30 @@ export async function onClaudeReviewCompleted(ctx, e) {
|
|
|
177
181
|
// The STORED trigger agrees, or nothing is sent (no claim either).
|
|
178
182
|
if (run.trigger !== 'auto')
|
|
179
183
|
return;
|
|
184
|
+
// A PAIR: its one summary and verdict, named "Claude + Codex" (an older pair reads as this half).
|
|
185
|
+
let verdict = run.verdict;
|
|
186
|
+
let summary = run.summary;
|
|
187
|
+
let ranBy = aiRunProviderName(run);
|
|
188
|
+
if (run.pairId != null) {
|
|
189
|
+
const rec = (await ctx.db
|
|
190
|
+
.select({ pairConsolidation: cr.pairConsolidation })
|
|
191
|
+
.from(cr)
|
|
192
|
+
.where(and(eq(cr.id, run.pairId), eq(cr.accountId, e.accountId), eq(cr.prId, e.prId)))
|
|
193
|
+
.limit(1)
|
|
194
|
+
.execute())[0]?.pairConsolidation;
|
|
195
|
+
if (rec != null && typeof rec === 'object') {
|
|
196
|
+
if (typeof rec.summary === 'string')
|
|
197
|
+
summary = rec.summary;
|
|
198
|
+
if (rec.verdict === 'APPROVE' || rec.verdict === 'REQUEST_CHANGES' || rec.verdict === 'COMMENT')
|
|
199
|
+
verdict = rec.verdict;
|
|
200
|
+
if (rec.how !== 'single')
|
|
201
|
+
ranBy = PAIR_RUN_LABEL;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
180
204
|
if (!(await claimNotice(ctx, e.accountId, 'review', e.reviewId)))
|
|
181
205
|
return;
|
|
182
206
|
try {
|
|
183
|
-
await postToSlack(target.row.webhookUrl, reviewMessage(ctx, target.workspaceId, facts,
|
|
207
|
+
await postToSlack(target.row.webhookUrl, reviewMessage(ctx, target.workspaceId, facts, verdict, summary, ranBy));
|
|
184
208
|
}
|
|
185
209
|
catch (err) {
|
|
186
210
|
ctx.log.warn({ err, accountId: e.accountId, workspaceId: target.workspaceId, reviewId: e.reviewId }, 'slack review signal send failed');
|
|
@@ -30,6 +30,7 @@ export function buildAgentContext(log) {
|
|
|
30
30
|
const r = detectProviderAuth(provider, kind);
|
|
31
31
|
return r.status === 'ok' ? { status: 'ok' } : { status: 'none', message: r.message };
|
|
32
32
|
},
|
|
33
|
+
complete: async (opts) => (await import('./llm.js')).cheapComplete(opts),
|
|
33
34
|
},
|
|
34
35
|
recordAiUsage: (row) => recordAiUsage(row),
|
|
35
36
|
github: {
|
package/dist/review/agentic.js
CHANGED
|
@@ -101,6 +101,11 @@ export async function startAgenticBackground(app) {
|
|
|
101
101
|
catch (err) {
|
|
102
102
|
app.log.warn({ err }, 'claude review: startup reconcile failed');
|
|
103
103
|
}
|
|
104
|
+
// TEAM CLAIMS: a run this process lost (a restart mid-review) left its limn/review-* line pending on
|
|
105
|
+
// GitHub; release every such line now, in the BACKGROUND (GitHub calls; never fails or delays the boot).
|
|
106
|
+
void import('./claude-review/team-claim-release.js')
|
|
107
|
+
.then((m) => m.releaseStaleTeamClaims(ctx))
|
|
108
|
+
.catch((err) => app.log.warn({ err }, 'team claim release at boot failed'));
|
|
104
109
|
try {
|
|
105
110
|
await reconcileThreadChecksOnStartup(ctx);
|
|
106
111
|
}
|
|
@@ -62,10 +62,13 @@ import {} from '../../shared/index.js';
|
|
|
62
62
|
import { readWorkspaceAutoPostForPr } from './auto-settings.js';
|
|
63
63
|
import { similarTitles } from './settled-by-reply.js';
|
|
64
64
|
import { prAuthorIsAccount } from '../../coding/ai-fix/auto-fix.js';
|
|
65
|
-
import { getClaudeReviewById, getReviewPostContext, isAlreadyPostedReraise, isReaderIgnoredFinding, markReviewPosted, } from './persist.js';
|
|
65
|
+
import { getClaudeReviewById, getReviewPostContext, isAlreadyPostedReraise, isReaderIgnoredFinding, markReviewPosted, readPairConsolidation, } from './persist.js';
|
|
66
66
|
import { resolveFindingBody } from './follow-up.js';
|
|
67
|
-
import {
|
|
67
|
+
import { autoVerdictFor } from './auto-verdict.js';
|
|
68
|
+
import { aiRunProviderName, autoPostFooter, findingSourceLabel, limnSeverityLabel, PAIR_AUTO_POST_FOOTER, } from '../../shared/index.js';
|
|
68
69
|
import { pairRaisedByLead, pairReviewBody, pairVerdict } from './pair.js';
|
|
70
|
+
// A pair's body names both when it falls back to a count ("3 comments from Claude and Codex.").
|
|
71
|
+
const PAIR_BODY_BY = 'Claude and Codex';
|
|
69
72
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
70
73
|
const s = (ctx) => ctx.schema;
|
|
71
74
|
/** May auto-posting write on this PR? null = yes; else why not. */
|
|
@@ -143,21 +146,8 @@ export function selectFindingsToPost(findings, kinds, earlier) {
|
|
|
143
146
|
}
|
|
144
147
|
return { toPost, alreadyPosted };
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* a reader did not ignore (whatever the kind toggles say): any blocker ⇒ REQUEST_CHANGES; APPROVE only when
|
|
149
|
-
* Claude approved and there is no warning either; everything else ⇒ COMMENT.
|
|
150
|
-
*/
|
|
151
|
-
export function autoVerdictFor(claudeVerdict, findings) {
|
|
152
|
-
// ⚠ Everything except a READER'S ignore counts — a re-raise the server left out because its
|
|
153
|
-
// comment is already on this commit is still an open issue (persist.ts `isReaderIgnoredFinding`).
|
|
154
|
-
const counted = findings.filter((f) => f.story == null && !isReaderIgnoredFinding(f));
|
|
155
|
-
if (counted.some((f) => f.severity === 'blocker'))
|
|
156
|
-
return 'REQUEST_CHANGES';
|
|
157
|
-
if (claudeVerdict === 'APPROVE' && !counted.some((f) => f.severity === 'warning'))
|
|
158
|
-
return 'APPROVE';
|
|
159
|
-
return 'COMMENT';
|
|
160
|
-
}
|
|
149
|
+
// THE VERDICT RULE (./auto-verdict.ts), re-exported under its old name.
|
|
150
|
+
export { autoVerdictFor } from './auto-verdict.js';
|
|
161
151
|
/** May an automatic APPROVE go out at this impact level? ⚠ 'unknown' is judged as HIGH. */
|
|
162
152
|
export function impactAllowsApprove(level, allowed) {
|
|
163
153
|
return allowed[level === 'unknown' ? 'high' : level];
|
|
@@ -355,9 +345,13 @@ export async function loadUnpostedBlockersOfBlock(ctx, accountId, prId, githubRe
|
|
|
355
345
|
.from(cr)
|
|
356
346
|
.where(and(eq(cr.prId, prId), eq(cr.accountId, accountId)))
|
|
357
347
|
.execute());
|
|
358
|
-
|
|
348
|
+
// Every run whose record carries this GitHub review: a pair's combined post is recorded on BOTH
|
|
349
|
+
// halves, and either half may hold the blocker that was posted (or merged away).
|
|
350
|
+
const blockRunIds = runs
|
|
351
|
+
.filter((r) => r.autoPost?.githubReviewId != null && String(r.autoPost.githubReviewId) === githubReviewId)
|
|
352
|
+
.map((r) => r.id);
|
|
359
353
|
// No such run: the caller only passes ids `loadLimnAutoBlockIds` read, so this is a race — hold.
|
|
360
|
-
if (
|
|
354
|
+
if (blockRunIds.length === 0)
|
|
361
355
|
return [-1];
|
|
362
356
|
const rows = (await ctx.db
|
|
363
357
|
.select({
|
|
@@ -371,7 +365,9 @@ export async function loadUnpostedBlockersOfBlock(ctx, accountId, prId, githubRe
|
|
|
371
365
|
storyRef: crf.storyRef,
|
|
372
366
|
})
|
|
373
367
|
.from(crf)
|
|
374
|
-
|
|
368
|
+
// A pair's merged-away duplicate is never posted and never read as a finding: its kept finding
|
|
369
|
+
// stands for it.
|
|
370
|
+
.where(and(inArray(crf.reviewId, blockRunIds), isNull(crf.mergedInto)))
|
|
375
371
|
.execute());
|
|
376
372
|
const onGithub = new Map(earlier.map((e) => [e.id, e.onGithub]));
|
|
377
373
|
return rows
|
|
@@ -491,9 +487,10 @@ async function writeRecord(ctx, reviewId, rec) {
|
|
|
491
487
|
const cr = s(ctx).claudeReviews;
|
|
492
488
|
await ctx.db.update(cr).set({ autoPost: rec }).where(eq(cr.id, reviewId)).execute();
|
|
493
489
|
}
|
|
494
|
-
// `
|
|
495
|
-
//
|
|
496
|
-
|
|
490
|
+
// `tag`: the comment rides a Claude + Codex pair's one review, so it names who raised it — "Claude",
|
|
491
|
+
// "Codex" or "Claude + Codex" (`findingSourceLabel`); null on an ordinary run. (Story findings are never
|
|
492
|
+
// auto-posted, so the lead slot is free.) `by` is the provider whose run holds the finding (its footer).
|
|
493
|
+
const toPostFinding = (f, by, tag = null) => ({
|
|
497
494
|
id: f.id,
|
|
498
495
|
path: f.path,
|
|
499
496
|
line: f.line,
|
|
@@ -505,8 +502,8 @@ const toPostFinding = (f, by, inPair = false) => ({
|
|
|
505
502
|
severity: f.severity,
|
|
506
503
|
// A pair's comment names who raised it ON the ONE severity lead ("**Warning** · Codex"); a finding
|
|
507
504
|
// with no label word (none posted today) falls back to its own "Raised by" line.
|
|
508
|
-
severityTag:
|
|
509
|
-
storyLead:
|
|
505
|
+
severityTag: tag,
|
|
506
|
+
storyLead: tag != null && limnSeverityLabel(f.severity) == null ? pairRaisedByLead(tag) : null,
|
|
510
507
|
// The footer names the provider that ran the review (Claude's text unchanged).
|
|
511
508
|
footer: autoPostFooter(by),
|
|
512
509
|
});
|
|
@@ -731,6 +728,15 @@ async function autoPostReview(ctx, { accountId, prId, reviewId, pairFirstReviewI
|
|
|
731
728
|
first = null;
|
|
732
729
|
}
|
|
733
730
|
const pairFirst = first;
|
|
731
|
+
// THE PAIR'S ONE REVIEW (pair-consolidate.ts): its summary, and who raised each kept finding. A pair
|
|
732
|
+
// from before the record reads as the two summaries joined and each half's own provider.
|
|
733
|
+
const consolidation = pairFirst != null ? await readPairConsolidation(ctx, pairFirst.id, prId).catch(() => null) : null;
|
|
734
|
+
const tagOf = (f) => {
|
|
735
|
+
if (pairFirst == null)
|
|
736
|
+
return null;
|
|
737
|
+
const src = consolidation?.sourceOf[String(f.id)];
|
|
738
|
+
return src != null ? findingSourceLabel(src) : (byOf.get(f.id) ?? aiRunProviderName(review));
|
|
739
|
+
};
|
|
734
740
|
const posted = new Set();
|
|
735
741
|
const errors = [];
|
|
736
742
|
const submit = (ev) => ctx.review.postReview({
|
|
@@ -739,17 +745,20 @@ async function autoPostReview(ctx, { accountId, prId, reviewId, pairFirstReviewI
|
|
|
739
745
|
prNumber: pctx.prNumber,
|
|
740
746
|
reviewHeadSha: pctx.reviewHeadSha,
|
|
741
747
|
body: pairFirst != null
|
|
742
|
-
?
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
748
|
+
? consolidation?.how === 'pass'
|
|
749
|
+
? `${autoReviewBodyLead(consolidation.summary, picked.toPost.length, PAIR_BODY_BY)}\n\n${PAIR_AUTO_POST_FOOTER}`
|
|
750
|
+
: // Joined in code (or an older pair): each half's lead, named.
|
|
751
|
+
pairReviewBody([pairFirst, review].map((r) => {
|
|
752
|
+
const mine = new Set(r.findings.map((x) => x.id));
|
|
753
|
+
const n = picked.toPost.filter((f) => mine.has(f.id)).length;
|
|
754
|
+
return { by: aiRunProviderName(r), text: autoReviewBodyLead(r.summary, n, aiRunProviderName(r)) };
|
|
755
|
+
}), PAIR_AUTO_POST_FOOTER)
|
|
747
756
|
: autoReviewBody(review.summary, picked.toPost.length, aiRunProviderName(review)),
|
|
748
757
|
// COMMENT unless auto verdict is on AND its gate let the verdict through.
|
|
749
758
|
verdict: ev,
|
|
750
759
|
// Questions ride the same review: the seam anchors each inline, else falls back. In a pair each
|
|
751
760
|
// comment carries its own provider's footer and names who raised it.
|
|
752
|
-
includedFindings: picked.toPost.map((f) => toPostFinding(f, byOf.get(f.id) ?? aiRunProviderName(review),
|
|
761
|
+
includedFindings: picked.toPost.map((f) => toPostFinding(f, byOf.get(f.id) ?? aiRunProviderName(review), tagOf(f))),
|
|
753
762
|
dryRun: false,
|
|
754
763
|
});
|
|
755
764
|
// Did the review half go wrong outright (thrown, or the head moved)? Then nothing more is written.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isReaderIgnoredFinding } from './finding-facts.js';
|
|
2
|
+
/**
|
|
3
|
+
* The verdict an auto post wants — Claude's, made STRICTER. Counts the run's non-story findings
|
|
4
|
+
* a reader did not ignore (whatever the kind toggles say): any blocker ⇒ REQUEST_CHANGES; APPROVE only when
|
|
5
|
+
* Claude approved and there is no warning either; everything else ⇒ COMMENT.
|
|
6
|
+
*/
|
|
7
|
+
export function autoVerdictFor(claudeVerdict, findings) {
|
|
8
|
+
// ⚠ Everything except a READER'S ignore counts — a re-raise the server left out because its
|
|
9
|
+
// comment is already on this commit is still an open issue (`isReaderIgnoredFinding`).
|
|
10
|
+
const counted = findings.filter((f) => f.story == null && !isReaderIgnoredFinding(f));
|
|
11
|
+
if (counted.some((f) => f.severity === 'blocker'))
|
|
12
|
+
return 'REQUEST_CHANGES';
|
|
13
|
+
if (claudeVerdict === 'APPROVE' && !counted.some((f) => f.severity === 'warning'))
|
|
14
|
+
return 'APPROVE';
|
|
15
|
+
return 'COMMENT';
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=auto-verdict.js.map
|
|
@@ -572,6 +572,11 @@ export function registerAutoReview(ctx) {
|
|
|
572
572
|
return;
|
|
573
573
|
ctx.registerScheduledJob(AUTO_REVIEW_CRON, async () => {
|
|
574
574
|
await runAutoReviewSweep(ctx);
|
|
575
|
+
// TEAM CLAIMS: release every limn/review-* line of ours no run holds any more, and teammates'
|
|
576
|
+
// expired ones (team-claim-release.ts). Never throws; never blocks the sweep.
|
|
577
|
+
await import('./team-claim-release.js')
|
|
578
|
+
.then((m) => m.releaseStaleTeamClaims(ctx))
|
|
579
|
+
.catch((err) => ctx.log.warn(`team claim release: ${err instanceof Error ? err.message : String(err)}`));
|
|
575
580
|
}, 'claude-auto-review');
|
|
576
581
|
}
|
|
577
582
|
//# sourceMappingURL=auto.js.map
|