pierre-review 0.1.53 → 0.1.55

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.
@@ -41,7 +41,7 @@ export async function meRoutes(app) {
41
41
  },
42
42
  feedLastSeenAt: feedLastSeen ? feedLastSeen.toISOString() : null,
43
43
  newFeedItems,
44
- claudeReviewEnabled: config.claudeReviewEnabled,
44
+ // Claude Review is now the Pro `claudeReview` capability (in `pro` below).
45
45
  deploymentMode: config.deploymentMode,
46
46
  pro: getProCapabilities(),
47
47
  };
package/dist/app.js CHANGED
@@ -19,7 +19,6 @@ import { feedRoutes } from './api/routes/feed.js';
19
19
  import { mergersRoutes } from './api/routes/mergers.js';
20
20
  import { insightsRoutes } from './api/routes/insights.js';
21
21
  import { activityRoutes } from './api/routes/activity.js';
22
- import { claudeReviewRoutes } from './api/routes/claude-review.js';
23
22
  export async function buildApp() {
24
23
  const app = Fastify({
25
24
  // In production (the installed CLI) the per-request "incoming request" /
@@ -132,10 +131,9 @@ export async function buildApp() {
132
131
  await app.register(mergersRoutes);
133
132
  await app.register(insightsRoutes);
134
133
  await app.register(activityRoutes);
135
- // Claude Review is local-only + opt-in. Only register its routes when enabled,
136
- // so the clone-manager / gh-CLI dependency is unreachable in cloud mode.
137
- if (config.claudeReviewEnabled)
138
- await app.register(claudeReviewRoutes);
134
+ // Claude Review moved into the @pierre/pro plugin (its routes register there, gated on the
135
+ // `claudeReview` capability). The SDK-run / diff-prep / GitHub-post infra + the tables stay
136
+ // in core behind the ctx.review seam; nothing to register here.
139
137
  return app;
140
138
  }
141
139
  //# sourceMappingURL=app.js.map
package/dist/config.js CHANGED
@@ -141,18 +141,16 @@ export const config = {
141
141
  digestMaxReposPerRefresh: intFromEnv('PRO_DIGEST_MAX_REPOS', 30),
142
142
  digestMinIntervalSec: intFromEnv('PRO_DIGEST_MIN_INTERVAL_SEC', 60),
143
143
  },
144
- // ---- Claude Review (agentic PR review; opt-in, LOCAL-ONLY) ----
145
- // OFF by default: the feature spends real money / Agent-SDK credits per run.
146
- // Enable with ENABLE_CLAUDE_REVIEW=true. FORCE-DISABLED in cloud mode (it
147
- // shells out to a local gh + writable clone dir that don't exist on Railway).
148
- claudeReviewEnabled: !isCloud && process.env.ENABLE_CLAUDE_REVIEW === 'true',
144
+ // ---- Claude Review — CORE infra knobs (the product moved to @pierre/pro) ----
145
+ // The SDK-run / diff-prep infra behind the ctx.review seam reads these. The old
146
+ // `claudeReviewEnabled` env flag was removed Claude Review is now the Pro
147
+ // `claudeReview` capability (PRO_CLAUDE_REVIEW_ENABLED); the routing thresholds,
148
+ // concurrency + queue caps, and the default-model picker moved to PRO_REVIEW_* env.
149
149
  // Partial clones + ephemeral worktrees live here (a user-writable home path,
150
150
  // never the read-only install dir). CLONE_DIR overrides.
151
151
  cloneDir: process.env.CLONE_DIR ?? resolve(homedir(), '.pierre-review', 'clones'),
152
152
  // Soft cap on the clone cache before LRU cleanup evicts idle repos (default 2 GiB).
153
153
  cloneCacheMaxBytes: intFromEnv('CLONE_CACHE_MAX_BYTES', 2 * 1024 * 1024 * 1024),
154
- // Default model for the picker; per-run model still overrides on the request.
155
- defaultReviewModel: process.env.DEFAULT_REVIEW_MODEL ?? 'claude-sonnet-4-6',
156
154
  // Per-run caps (cost/disk/time runaway guards). The diff is inlined in full, so
157
155
  // reviews need far fewer turns than the old default; 30 is still generous.
158
156
  reviewMaxTurns: intFromEnv('REVIEW_MAX_TURNS', 30),
@@ -191,16 +189,8 @@ export const config = {
191
189
  // Applied only to effort-capable models (Sonnet/Opus); Haiku ignores it.
192
190
  reviewEffort: effortFromEnv('REVIEW_EFFORT', 'medium'),
193
191
  reviewDiffOnlyEffort: effortFromEnv('REVIEW_DIFF_ONLY_EFFORT', 'low'),
194
- // At most one review per PR; this caps concurrent reviews across all PRs. Default
195
- // 4 so the user can bulk-review (extras queue, see review-manager). Raising this
196
- // also DISABLES the per-run env auth adjustment (which mutates process.env and is
197
- // only safe at concurrency 1 — see auth.applyClaudeReviewAuth); the raw ambient
198
- // env is used instead. Set REVIEW_CONCURRENCY=1 to restore prefer-ambient + the
199
- // API-key fallback.
200
- reviewConcurrency: intFromEnv('REVIEW_CONCURRENCY', 4),
201
- // Hard ceiling on QUEUED (not-yet-started) reviews, a runaway guard for bulk
202
- // triggering; further starts return 'busy' until the queue drains.
203
- reviewMaxQueued: intFromEnv('REVIEW_MAX_QUEUED', 50),
192
+ // (Review concurrency + queue caps + the routing thresholds moved to the plugin's
193
+ // PRO_REVIEW_* env the plugin owns the queue/manager + the mode-routing decision.)
204
194
  // ---- AI Fix (Pro: agentic code fixer) — core infra knobs ----
205
195
  // The write-capable fixer (packages/pro/ai-fix) reuses the review clone/agent
206
196
  // machinery. A runaway fixer is pricier than a review (it edits + re-reads files),
@@ -217,26 +207,6 @@ export const config = {
217
207
  // rebased commit that conflicts gets one resolver pass, up to this many steps, then
218
208
  // we abort the rebase rather than loop forever on a pathological history.
219
209
  aiFixRebaseMaxSteps: intFromEnv('AI_FIX_REBASE_MAX_STEPS', 10),
220
- // ---- Claude Review routing (diff-only vs worktree) — THE THRESHOLDS ----
221
- // The deterministic pre-check (review/routing.ts) decides, BEFORE the agent runs,
222
- // whether a PR can be reviewed from its diff alone (fast, tool-less, no worktree)
223
- // or needs the full cloned worktree as explorable context. A change stays
224
- // 'diff_only' only if it is within EVERY ceiling below AND touches no exported/
225
- // public API; otherwise it routes to 'worktree'. The numbers are deliberately
226
- // CONSERVATIVE — any tie routes to worktree, since over-reviewing is safe but
227
- // under-reviewing a risky change is not. Every decision input is logged on the
228
- // run (claude_reviews.route_reason) so these can be tuned against the agent's own
229
- // scopeUsed self-report. This is the single place to review/adjust the thresholds.
230
- reviewRouting: {
231
- // Max (non-noise) files changed for a diff-only review.
232
- maxFiles: intFromEnv('REVIEW_ROUTE_MAX_FILES', 5),
233
- // Max added + deleted lines (non-noise files) for a diff-only review.
234
- maxLines: intFromEnv('REVIEW_ROUTE_MAX_LINES', 150),
235
- // Max distinct directories touched for a diff-only review.
236
- maxDirs: intFromEnv('REVIEW_ROUTE_MAX_DIRS', 2),
237
- // Max distinct top-level subsystems (first path segment) for a diff-only review.
238
- maxSubsystems: intFromEnv('REVIEW_ROUTE_MAX_SUBSYSTEMS', 1),
239
- },
240
210
  };
241
211
  // Fail loud at startup if a required cloud var is missing — mirrors the
242
212
  // gh-auth loud failure. Called from index.ts only when deploymentMode==='cloud'.
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { and, asc, count, desc, eq, exists, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql, } from 'drizzle-orm';
3
2
  // Local copy of the shared `REASON_PRIORITY` value constant. `@pierre-review/shared`
4
3
  // is a types-only workspace package that is NOT shipped in the published tarball,
@@ -16,6 +15,8 @@ const REASON_PRIORITY = [
16
15
  import { db, schema, isPg } from './client.js';
17
16
  import { runTransaction } from './client.js';
18
17
  import { config } from '../config.js';
18
+ import { getProCapabilities } from '../pro/contract.js';
19
+ import { createHash } from 'node:crypto';
19
20
  import { computeApprovalInfoByPr, computeTriage, } from './triage.js';
20
21
  import { getAccountUserId } from '../auth/account.js';
21
22
  import { ensureRoutingPrFiles } from '../sync/routing-files.js';
@@ -30,6 +31,11 @@ function iso(d) {
30
31
  function emptyCounts() {
31
32
  return { resolved: 0, likely_addressed: 0, replied_unresolved: 0, untouched: 0 };
32
33
  }
34
+ // GitHub anchors a file in a PR's "Files changed" diff by the SHA-256 of its path; used to
35
+ // deep-link the Changes tab's per-file rows (getPrDetail).
36
+ function diffAnchorId(path) {
37
+ return createHash('sha256').update(path, 'utf8').digest('hex');
38
+ }
33
39
  function mapUser(u) {
34
40
  return {
35
41
  id: u.id,
@@ -1441,7 +1447,7 @@ function myTurnReasonsFor(participation, prId) {
1441
1447
  // Gated on the feature flag (force-off in cloud) so no items appear where Claude Review
1442
1448
  // doesn't exist. Never member-/bot-filtered (a run has no member author).
1443
1449
  async function getClaudeReviewFeedItems(accountId, repoIds, since) {
1444
- if (!config.claudeReviewEnabled)
1450
+ if (!getProCapabilities().claudeReview)
1445
1451
  return [];
1446
1452
  const conds = [
1447
1453
  eq(repos.accountId, accountId),
@@ -2979,7 +2985,7 @@ async function getActionableActivityIds(accountId) {
2979
2985
  for (const r of await listRepos(accountId))
2980
2986
  repoNameById.set(r.id, r.fullName);
2981
2987
  const threadIds = new Set((await getThreadsAwaiting(localUserId, accountId, repoNameById)).map((ta) => ta.threadId));
2982
- const claudeReviewIds = config.claudeReviewEnabled
2988
+ const claudeReviewIds = getProCapabilities().claudeReview
2983
2989
  ? new Set((await getUnactionedClaudeReviews(accountId)).map((c) => c.reviewId))
2984
2990
  : new Set();
2985
2991
  return { reviewRequestPrIds, watchedPrIds, approvedPrIds, threadIds, claudeReviewIds };
@@ -3138,7 +3144,7 @@ export async function getMyTurn(accountId) {
3138
3144
  }
3139
3145
  // Completed-but-unactioned Claude reviews (local-only feature; empty otherwise).
3140
3146
  // A manual "Done" hides the run until a newer run finishes (see claudeDismissedIds).
3141
- const claudeReviewsToAction = config.claudeReviewEnabled
3147
+ const claudeReviewsToAction = getProCapabilities().claudeReview
3142
3148
  ? (await getUnactionedClaudeReviews(accountId)).filter((c) => !claudeDismissedIds.has(c.reviewId))
3143
3149
  : [];
3144
3150
  const users = referencedUsers.size > 0
@@ -3723,138 +3729,24 @@ export async function setRepoInboxWatch(accountId, repoId, watch) {
3723
3729
  .execute();
3724
3730
  return updated.length > 0;
3725
3731
  }
3726
- // GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its
3727
- // path; we expose it so a finding can deep-link into the PR diff.
3728
- function diffAnchorId(path) {
3729
- return createHash('sha256').update(path, 'utf8').digest('hex');
3730
- }
3731
- function mapFinding(r) {
3732
- return {
3733
- id: r.id,
3734
- reviewId: r.reviewId,
3735
- path: r.path,
3736
- line: r.line,
3737
- side: r.side,
3738
- diffAnchorId: diffAnchorId(r.path),
3739
- severity: r.severity,
3740
- title: r.title,
3741
- body: r.body,
3742
- editedBody: r.editedBody,
3743
- suggestion: r.suggestion,
3744
- diffHunk: r.diffHunk,
3745
- anchored: r.anchored,
3746
- fileInDiff: r.fileInDiff,
3747
- included: r.included,
3748
- postedAt: iso(r.postedAt),
3749
- githubCommentId: r.githubCommentId,
3750
- postedCommentKind: r.postedCommentKind,
3751
- createdAt: r.createdAt.toISOString(),
3752
- };
3753
- }
3754
- function mapReview(r, findings) {
3755
- return {
3756
- id: r.id,
3757
- prId: r.prId,
3758
- headSha: r.headSha,
3759
- status: r.status,
3760
- model: r.model,
3761
- scope: r.scope,
3762
- reviewMode: r.reviewMode,
3763
- routeReason: r.routeReason,
3764
- summary: r.summary,
3765
- verdict: r.verdict,
3766
- userBody: r.userBody,
3767
- userVerdict: r.userVerdict,
3768
- costUsd: r.costUsd,
3769
- inputTokens: r.inputTokens,
3770
- outputTokens: r.outputTokens,
3771
- cacheReadTokens: r.cacheReadTokens,
3772
- cacheCreationTokens: r.cacheCreationTokens,
3773
- numTurns: r.numTurns,
3774
- diffBytes: r.diffBytes,
3775
- diffCapped: r.diffCapped,
3776
- error: r.error,
3777
- excludedFiles: r.excludedFiles ?? [],
3778
- postedReviewId: r.postedReviewId,
3779
- postedAt: iso(r.postedAt),
3780
- createdAt: r.createdAt.toISOString(),
3781
- finishedAt: iso(r.finishedAt),
3782
- findings: findings.map(mapFinding),
3783
- };
3784
- }
3785
- export async function getClaudeReviewById(reviewId, accountId) {
3786
- const rows = await db
3787
- .select({ review: claudeReviews })
3788
- .from(claudeReviews)
3789
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
3790
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
3791
- .where(and(eq(claudeReviews.id, reviewId), eq(repos.accountId, accountId)))
3792
- .limit(1)
3793
- .execute();
3794
- const row = rows[0]?.review ?? null;
3795
- if (!row)
3796
- return null;
3797
- const findings = await db
3798
- .select()
3799
- .from(claudeReviewFindings)
3800
- .where(eq(claudeReviewFindings.reviewId, reviewId))
3801
- .orderBy(asc(claudeReviewFindings.id))
3802
- .execute();
3803
- return mapReview(row, findings);
3804
- }
3805
- // The most recent run for a PR (with findings), or null if never run.
3806
- export async function getLatestClaudeReview(prId, accountId) {
3807
- const rows = await db
3808
- .select({ id: claudeReviews.id })
3809
- .from(claudeReviews)
3810
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
3811
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
3812
- .where(and(eq(claudeReviews.prId, prId), eq(repos.accountId, accountId)))
3813
- .orderBy(desc(claudeReviews.id))
3814
- .limit(1)
3815
- .execute();
3816
- const row = rows[0] ?? null;
3817
- if (!row)
3818
- return null;
3819
- return getClaudeReviewById(row.id, accountId);
3820
- }
3821
- // All runs for a PR (newest first), lighter shape for the history selector.
3822
- export async function listClaudeReviewHistory(prId, accountId) {
3823
- const rows = await db
3824
- .select({ review: claudeReviews })
3825
- .from(claudeReviews)
3826
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
3827
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
3828
- .where(and(eq(claudeReviews.prId, prId), eq(repos.accountId, accountId)))
3829
- .orderBy(desc(claudeReviews.id))
3830
- .execute();
3831
- return rows.map(({ review: r }) => ({
3832
- id: r.id,
3833
- headSha: r.headSha,
3834
- status: r.status,
3835
- model: r.model,
3836
- scope: r.scope,
3837
- reviewMode: r.reviewMode,
3838
- verdict: r.verdict,
3839
- userVerdict: r.userVerdict,
3840
- costUsd: r.costUsd,
3841
- postedAt: iso(r.postedAt),
3842
- createdAt: r.createdAt.toISOString(),
3843
- finishedAt: iso(r.finishedAt),
3844
- }));
3845
- }
3732
+ // ---- Claude Review reads (CORE-owned surfaces only) ----
3733
+ // The FEATURE-only reads (getClaudeReviewById/Latest/History/listAll + the post/PR
3734
+ // contexts + mapReview/mapFinding) MOVED to packages/pro/src/claude-review/persist.ts
3735
+ // when Claude Review became a Pro capability. The two readers below stay CORE because
3736
+ // CORE surfaces consume them: listClaudeReviewsByRepo (Activity console) +
3737
+ // getUnactionedClaudeReviews (My-Turn inbox). Both read the still-core tables.
3846
3738
  // Repo-oriented Claude-review retrieval for the Activity single-repo console: ALL runs
3847
3739
  // for a repo's PRs, grouped by PR (newest run first within each), PRs ordered by
3848
3740
  // most-recent run desc. Richer than listAllClaudeReviews (which keeps only one
3849
3741
  // latest-succeeded run per PR). IDOR-sensitive id getter: scoped by accountId, and
3850
- // gated on config.claudeReviewEnabled. An unowned repo (cross-account) → empty list.
3742
+ // gated on getProCapabilities().claudeReview. An unowned repo (cross-account) → empty list.
3851
3743
  export async function listClaudeReviewsByRepo(repoId, accountId) {
3852
- if (!config.claudeReviewEnabled)
3744
+ if (!getProCapabilities().claudeReview)
3853
3745
  return { enabled: false, prs: [] };
3854
3746
  // Ownership: a repo not owned by this account leaks nothing (404-equivalent).
3855
3747
  const owned = await getRepo(repoId, accountId);
3856
3748
  if (!owned)
3857
- return { enabled: config.claudeReviewEnabled, prs: [] };
3749
+ return { enabled: getProCapabilities().claudeReview, prs: [] };
3858
3750
  const rows = await db
3859
3751
  .select({
3860
3752
  review: claudeReviews,
@@ -3902,60 +3794,6 @@ export async function listClaudeReviewsByRepo(repoId, accountId) {
3902
3794
  }
3903
3795
  return { enabled: true, prs: [...byPr.values()] };
3904
3796
  }
3905
- // Cross-PR list of prior Claude reviews: ONE entry per PR = that PR's most-recent
3906
- // SUCCEEDED run, accountId-scoped, restricted to PRs still within the timeline
3907
- // window (open, or last touched within `backfillDays`), newest-first by finish
3908
- // time. Used by GET /api/claude-reviews to populate the "prior reviews" view.
3909
- export async function listAllClaudeReviews(accountId) {
3910
- // Same window cutoff getTimeline uses for its overlap predicate (now − backfillDays).
3911
- const cutoff = new Date(Date.now() - config.backfillDays * 24 * 60 * 60 * 1000);
3912
- const rows = await db
3913
- .select({
3914
- reviewId: claudeReviews.id,
3915
- prId: claudeReviews.prId,
3916
- owner: repos.owner,
3917
- name: repos.name,
3918
- prNumber: pullRequests.number,
3919
- prTitle: pullRequests.title,
3920
- prState: pullRequests.state,
3921
- summary: claudeReviews.summary,
3922
- verdict: claudeReviews.verdict,
3923
- headSha: claudeReviews.headSha,
3924
- status: claudeReviews.status,
3925
- createdAt: claudeReviews.createdAt,
3926
- finishedAt: claudeReviews.finishedAt,
3927
- })
3928
- .from(claudeReviews)
3929
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
3930
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
3931
- .where(and(eq(repos.accountId, accountId), eq(claudeReviews.status, 'succeeded'), or(eq(pullRequests.state, 'open'), gte(sql `coalesce(${pullRequests.mergedAt}, ${pullRequests.closedAt}, ${pullRequests.openedAt})`, tsBound(cutoff)))))
3932
- .orderBy(desc(claudeReviews.finishedAt), desc(claudeReviews.createdAt))
3933
- .execute();
3934
- // Keep the first (most-recent) succeeded run per PR. N is small (single local
3935
- // user), so a JS pass is simpler and portable across both dialects.
3936
- const seen = new Set();
3937
- const items = [];
3938
- for (const r of rows) {
3939
- if (seen.has(r.prId))
3940
- continue;
3941
- seen.add(r.prId);
3942
- items.push({
3943
- reviewId: r.reviewId,
3944
- prId: r.prId,
3945
- repoFullName: `${r.owner}/${r.name}`,
3946
- prNumber: r.prNumber,
3947
- prTitle: r.prTitle,
3948
- prState: r.prState,
3949
- summary: r.summary,
3950
- verdict: r.verdict,
3951
- headSha: r.headSha,
3952
- status: r.status,
3953
- createdAt: r.createdAt.toISOString(),
3954
- finishedAt: iso(r.finishedAt),
3955
- });
3956
- }
3957
- return items;
3958
- }
3959
3797
  // Completed Claude reviews on OPEN PRs that haven't been actioned: each PR's
3960
3798
  // MOST-RECENT succeeded run, kept only when it was never posted (postedAt null).
3961
3799
  // Account-scoped. Feeds the My Turn "Claude reviews to action" section so finished
@@ -4003,91 +3841,6 @@ export async function getUnactionedClaudeReviews(accountId) {
4003
3841
  }
4004
3842
  return out;
4005
3843
  }
4006
- export async function getFindingPostContext(findingId, accountId) {
4007
- const rows = await db
4008
- .select({
4009
- finding: claudeReviewFindings,
4010
- reviewId: claudeReviews.id,
4011
- reviewHeadSha: claudeReviews.headSha,
4012
- owner: repos.owner,
4013
- name: repos.name,
4014
- prNumber: pullRequests.number,
4015
- })
4016
- .from(claudeReviewFindings)
4017
- .innerJoin(claudeReviews, eq(claudeReviews.id, claudeReviewFindings.reviewId))
4018
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
4019
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
4020
- .where(and(eq(claudeReviewFindings.id, findingId), eq(repos.accountId, accountId)))
4021
- .limit(1)
4022
- .execute();
4023
- const row = rows[0] ?? null;
4024
- if (!row)
4025
- return null;
4026
- return {
4027
- finding: row.finding,
4028
- reviewId: row.reviewId,
4029
- reviewHeadSha: row.reviewHeadSha,
4030
- owner: row.owner,
4031
- name: row.name,
4032
- prNumber: row.prNumber,
4033
- };
4034
- }
4035
- export async function getClaudeReviewContext(reviewId, accountId) {
4036
- const rows = await db
4037
- .select({
4038
- review: claudeReviews,
4039
- owner: repos.owner,
4040
- name: repos.name,
4041
- prNumber: pullRequests.number,
4042
- })
4043
- .from(claudeReviews)
4044
- .innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
4045
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
4046
- .where(and(eq(claudeReviews.id, reviewId), eq(repos.accountId, accountId)))
4047
- .limit(1)
4048
- .execute();
4049
- const row = rows[0] ?? null;
4050
- if (!row)
4051
- return null;
4052
- return {
4053
- review: row.review,
4054
- owner: row.owner,
4055
- name: row.name,
4056
- prNumber: row.prNumber,
4057
- };
4058
- }
4059
- export async function getReviewPrContext(prId, accountId) {
4060
- const rows = await db
4061
- .select({
4062
- prId: pullRequests.id,
4063
- owner: repos.owner,
4064
- name: repos.name,
4065
- number: pullRequests.number,
4066
- title: pullRequests.title,
4067
- body: pullRequests.body,
4068
- baseRefName: pullRequests.baseRefName,
4069
- headSha: pullRequests.headSha,
4070
- })
4071
- .from(pullRequests)
4072
- .innerJoin(repos, eq(repos.id, pullRequests.repoId))
4073
- .where(and(eq(pullRequests.id, prId), eq(repos.accountId, accountId)))
4074
- .limit(1)
4075
- .execute();
4076
- const row = rows[0] ?? null;
4077
- if (!row)
4078
- return null;
4079
- return {
4080
- prId: row.prId,
4081
- owner: row.owner,
4082
- name: row.name,
4083
- repoFullName: `${row.owner}/${row.name}`,
4084
- number: row.number,
4085
- title: row.title,
4086
- body: row.body,
4087
- baseRefName: row.baseRefName,
4088
- headSha: row.headSha,
4089
- };
4090
- }
4091
3844
  // Resolve a review thread to its GitHub node id + parent-PR coordinates, for a
4092
3845
  // reply (addPullRequestReviewThreadReply) or resolve/unresolve mutation. The
4093
3846
  // thread's GitHub node id is the GraphQL `pullRequestReviewThreadId` / `threadId`.
package/dist/index.js CHANGED
@@ -28,12 +28,8 @@ export async function start() {
28
28
  console.warn('local user unknown (gh api user failed) — "my turn" disabled');
29
29
  }
30
30
  const app = await buildApp();
31
- // Heal any Claude review runs left mid-flight by a crash (their 'running'
32
- // status is persisted). Only relevant when the feature is enabled.
33
- if (config.claudeReviewEnabled) {
34
- const { reconcileReviewsOnStartup } = await import('./review/review-manager.js');
35
- await reconcileReviewsOnStartup(app.log);
36
- }
31
+ // (Claude Review moved into @pierre/pro its crash-orphan reconcile now runs inside
32
+ // plugin.register during bindProPlugin below, alongside the AI-Fix reconcile.)
37
33
  // Bind the optional Pro plugin (dynamic import; no-ops in OSS mode). Same
38
34
  // "optional subsystem, degrade gracefully" posture as the scheduler below.
39
35
  {
package/dist/pro/bind.js CHANGED
@@ -9,6 +9,7 @@ import { recordAiUsage, getAiUsageSummary } from '../db/usage.js';
9
9
  import { reviewEvents, registerLearningsProvider } from '../review/events.js';
10
10
  import { cheapComplete } from '../review/llm.js';
11
11
  import { detectClaudeAuth } from '../review/auth.js';
12
+ import { hasUserAnthropicKey, setUserAnthropicKey, } from '../review/local-settings.js';
12
13
  import { getAccessToken } from '../auth/account.js';
13
14
  import { createPullRequest, fetchPrHeadInfo, fetchPrUnifiedDiff, } from '../github/mutations.js';
14
15
  import { fetchActionsJobLog } from '../github/actions-logs.js';
@@ -42,7 +43,7 @@ export async function bindProPlugin(app) {
42
43
  if (!mod)
43
44
  return;
44
45
  const plugin = (mod.default ?? mod);
45
- if (plugin?.apiVersion !== 6 || typeof plugin.register !== 'function') {
46
+ if (plugin?.apiVersion !== 7 || typeof plugin.register !== 'function') {
46
47
  app.log.warn({ apiVersion: plugin?.apiVersion }, 'pro contract mismatch — skipped');
47
48
  return;
48
49
  }
@@ -105,6 +106,19 @@ export async function bindProPlugin(app) {
105
106
  mergeResolveAndPush: async (a) => (await import('../coding/merge.js')).mergeResolveAndPush(a),
106
107
  pushResolved: async (a) => (await import('../coding/merge.js')).pushResolved(a),
107
108
  },
109
+ // Claude Review infra: diff prep + the SDK run + the GitHub review POST. Lazy so the
110
+ // Agent SDK (agent.js) loads only when a review actually runs, not at every boot.
111
+ review: {
112
+ prepareReview: async (a) => (await import('../review/prepare.js')).prepareReview(a),
113
+ runReview: async (a) => (await import('../review/agent.js')).runReview(a),
114
+ postReview: async (a) => (await import('../review/post-seam.js')).postReview(a),
115
+ postFinding: async (a) => (await import('../review/post-seam.js')).postFinding(a),
116
+ getLocalKeyStatus: () => ({ hasUserKey: hasUserAnthropicKey() }),
117
+ setLocalKey: (k) => {
118
+ setUserAnthropicKey(k ?? '');
119
+ return { hasUserKey: hasUserAnthropicKey(), auth: detectClaudeAuth().status };
120
+ },
121
+ },
108
122
  };
109
123
  try {
110
124
  setProCapabilities(await plugin.register(app, ctx));
@@ -7,6 +7,7 @@ const EMPTY = {
7
7
  aiAnalysis: false,
8
8
  aiFix: false,
9
9
  teamInsights: false,
10
+ claudeReview: false,
10
11
  };
11
12
  let active = EMPTY;
12
13
  export function setProCapabilities(c) {