ensemble-review 1.0.35 → 1.0.36

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.
Files changed (129) hide show
  1. package/package.json +5 -2
  2. package/dist/ai/check-tool.js +0 -6
  3. package/dist/api/routes/auth.js +0 -194
  4. package/dist/api/routes/billing.js +0 -209
  5. package/dist/api/routes/claude-review.js +0 -522
  6. package/dist/api/routes/contact.js +0 -318
  7. package/dist/api/routes/feed.js +0 -21
  8. package/dist/api/routes/inbox.js +0 -45
  9. package/dist/api/routes/mentions.js +0 -22
  10. package/dist/api/routes/teams.js +0 -150
  11. package/dist/api/routes/webhooks.js +0 -181
  12. package/dist/coding/activity.js +0 -80
  13. package/dist/coding/agent.js +0 -147
  14. package/dist/coding/ai-fix/checks-preview.js +0 -58
  15. package/dist/coding/ai-fix/comment-seed.js +0 -519
  16. package/dist/coding/ai-fix/prompts.js +0 -283
  17. package/dist/coding/ai-resolve/prompt.js +0 -161
  18. package/dist/coding/ai-resolve/routes.js +0 -97
  19. package/dist/coding/ai-resolve/run.js +0 -343
  20. package/dist/coding/ai-resolve/schema.js +0 -26
  21. package/dist/coding/ai-resolve/validate.js +0 -136
  22. package/dist/coding/checks/deny-scan.js +0 -75
  23. package/dist/coding/checks/discover.js +0 -541
  24. package/dist/coding/checks/docker.js +0 -38
  25. package/dist/coding/checks/env.js +0 -88
  26. package/dist/coding/checks/hooks.js +0 -91
  27. package/dist/coding/checks/index.js +0 -8
  28. package/dist/coding/checks/local.js +0 -40
  29. package/dist/coding/checks/sandbox.js +0 -206
  30. package/dist/coding/checks/session.js +0 -518
  31. package/dist/coding/checks/spawn.js +0 -123
  32. package/dist/coding/checks/spec.js +0 -140
  33. package/dist/coding/checks/types.js +0 -6
  34. package/dist/coding/partial-spend.js +0 -19
  35. package/dist/coding/safe-path.js +0 -75
  36. package/dist/coding/story-build/build-prompts.js +0 -178
  37. package/dist/coding/story-build/build.js +0 -332
  38. package/dist/coding/story-build/checkouts.js +0 -66
  39. package/dist/coding/story-build/docs-runner.js +0 -178
  40. package/dist/coding/story-build/docs.js +0 -334
  41. package/dist/coding/story-build/manager.js +0 -999
  42. package/dist/coding/story-build/plan-agent.js +0 -190
  43. package/dist/coding/story-build/plan.js +0 -338
  44. package/dist/coding/story-build/prompts.js +0 -147
  45. package/dist/coding/story-build/routes-docs.js +0 -113
  46. package/dist/coding/story-build/routes.js +0 -295
  47. package/dist/coding/story-build/schema.js +0 -53
  48. package/dist/db/automation-output.js +0 -349
  49. package/dist/db/credits.js +0 -79
  50. package/dist/db/schema.js +0 -321
  51. package/dist/db/schema.pg.js +0 -2197
  52. package/dist/db/sprint-repo-suggest.js +0 -257
  53. package/dist/db/sprint-stories.js +0 -42
  54. package/dist/db/sprint-story.js +0 -505
  55. package/dist/db/sprint-ticket-repos.js +0 -195
  56. package/dist/db/story-selection.js +0 -270
  57. package/dist/db/team-comparison.js +0 -124
  58. package/dist/db/workspace-comparison.js +0 -98
  59. package/dist/feed/fyi-provider.js +0 -10
  60. package/dist/github/local-user.js +0 -92
  61. package/dist/github/repo-files.js +0 -26
  62. package/dist/ml/worker.js +0 -31
  63. package/dist/platform/advisor/discovery/tier2.js +0 -88
  64. package/dist/platform/advisor/discovery/tier3.js +0 -57
  65. package/dist/platform/advisor/emitters/adapter.js +0 -5
  66. package/dist/platform/advisor/emitters/coderabbit.js +0 -175
  67. package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
  68. package/dist/platform/advisor/emitters/greptile.js +0 -176
  69. package/dist/platform/advisor/emitters/lint.js +0 -37
  70. package/dist/platform/advisor/emitters/manifest.js +0 -239
  71. package/dist/platform/advisor/emitters/prose.js +0 -183
  72. package/dist/platform/advisor/emitters/registry.js +0 -20
  73. package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
  74. package/dist/platform/advisor/evidence.js +0 -79
  75. package/dist/platform/advisor/findings.js +0 -15
  76. package/dist/platform/advisor/intents.js +0 -269
  77. package/dist/platform/advisor/measure/effect.js +0 -18
  78. package/dist/platform/advisor/outputs/brief.js +0 -68
  79. package/dist/platform/advisor/outputs/config-pr.js +0 -156
  80. package/dist/platform/advisor/refine/diff-guard.js +0 -38
  81. package/dist/platform/advisor/refine/refine.js +0 -35
  82. package/dist/platform/advisor/retro-check.js +0 -98
  83. package/dist/platform/advisor/routes.js +0 -648
  84. package/dist/platform/advisor/store.js +0 -321
  85. package/dist/platform/advisor/taxonomy.js +0 -67
  86. package/dist/platform/annotations/prompts.js +0 -84
  87. package/dist/platform/annotations/routes.js +0 -86
  88. package/dist/platform/annotations/runner.js +0 -234
  89. package/dist/platform/annotations/targets.js +0 -203
  90. package/dist/platform/insights/grounded-figures.js +0 -162
  91. package/dist/platform/insights/period-questions.js +0 -235
  92. package/dist/platform/work-plan/hash.js +0 -99
  93. package/dist/platform/work-plan/prompt.js +0 -209
  94. package/dist/platform/work-plan/routes.js +0 -350
  95. package/dist/pro/bind.js +0 -309
  96. package/dist/pro/contract.js +0 -36
  97. package/dist/pro/event-hooks.js +0 -37
  98. package/dist/pro/migrate.js +0 -64
  99. package/dist/review/events.js +0 -24
  100. package/dist/review/local-settings.js +0 -78
  101. package/dist/review/memory/capture.js +0 -0
  102. package/dist/review/memory/retrieval.js +0 -272
  103. package/dist/review/memory/routes.js +0 -78
  104. package/dist/review/persist.js +0 -203
  105. package/dist/review/plugin-providers.js +0 -12
  106. package/dist/review/prompt.js +0 -264
  107. package/dist/review/review-manager.js +0 -259
  108. package/dist/review/routing.js +0 -164
  109. package/dist/review/sprint-assess/agent.js +0 -145
  110. package/dist/review/sprint-assess/manager.js +0 -382
  111. package/dist/review/sprint-assess/prompts.js +0 -99
  112. package/dist/review/sprint-assess/routes.js +0 -109
  113. package/dist/review/sprint-assess/schema.js +0 -70
  114. package/dist/review/sprint-assess/validate.js +0 -180
  115. package/dist/review/sprint-story/manager.js +0 -98
  116. package/dist/review/sprint-story/prompt.js +0 -45
  117. package/dist/review/sprint-story/validate.js +0 -127
  118. package/dist/review/story-docs/build-prompts.js +0 -153
  119. package/dist/review/story-docs/build-validate.js +0 -162
  120. package/dist/review/story-report/agent.js +0 -203
  121. package/dist/review/story-report/fingerprint.js +0 -45
  122. package/dist/review/story-report/manager.js +0 -287
  123. package/dist/review/story-report/routes.js +0 -154
  124. package/dist/review/story-report/story-detail.js +0 -148
  125. package/dist/review/story-report/validate.js +0 -105
  126. package/dist/review/ticket-review/sweep-waits.js +0 -20
  127. package/dist/review/write-deny.js +0 -170
  128. package/dist/shared/conflict-ai.js +0 -24
  129. package/dist/sync/benchmark-rollup.js +0 -50
@@ -1,522 +0,0 @@
1
- import { config } from '../../config.js';
2
- import { getClaudeReviewById, getClaudeReviewContext, getFindingPostContext, getLatestClaudeReview, listAllClaudeReviews, listClaudeReviewHistory, } from '../../db/queries.js';
3
- import { markFindingPosted, markReviewPosted, updateFinding, updateReviewDraft, } from '../../review/persist.js';
4
- import { getReviewStatus, listActiveReviews, requestReviewCancel, startReview, subscribeReviewStream, } from '../../review/review-manager.js';
5
- import { detectClaudeAuth } from '../../review/auth.js';
6
- import { hasUserAnthropicKey, setUserAnthropicKey, } from '../../review/local-settings.js';
7
- import { buildAnchorIndex, buildReview, fallbackAnchor, fetchCurrentHeadSha, fetchPrDiff, findingCommentBody, prLevelFindingBody, stripNoiseFromDiff, submitGithubComment, submitGithubIssueComment, submitGithubReview, } from '../../review/post-review.js';
8
- import { isNoiseFile } from '../../review/prompt.js';
9
- import { accountIdOf } from '../plugins/auth.js';
10
- import { reviewEvents } from '../../review/events.js';
11
- const MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
12
- const VERDICTS = ['COMMENT', 'REQUEST_CHANGES', 'APPROVE'];
13
- const REVIEW_MODES = ['auto', 'diff_only', 'worktree'];
14
- const idParam = {
15
- params: {
16
- type: 'object',
17
- required: ['id'],
18
- properties: { id: { type: 'integer' } },
19
- },
20
- };
21
- const reviewIdParam = {
22
- params: {
23
- type: 'object',
24
- required: ['reviewId'],
25
- properties: { reviewId: { type: 'integer' } },
26
- },
27
- };
28
- const findingIdParam = {
29
- params: {
30
- type: 'object',
31
- required: ['findingId'],
32
- properties: { findingId: { type: 'integer' } },
33
- },
34
- };
35
- const generateSchema = {
36
- ...idParam,
37
- body: {
38
- type: 'object',
39
- required: ['model'],
40
- additionalProperties: false,
41
- properties: {
42
- model: { type: 'string', enum: MODELS },
43
- // Review depth. Omitted defaults to 'auto' (the router decides).
44
- mode: { type: 'string', enum: REVIEW_MODES },
45
- },
46
- },
47
- };
48
- const updateReviewSchema = {
49
- ...reviewIdParam,
50
- body: {
51
- type: 'object',
52
- additionalProperties: false,
53
- properties: {
54
- userBody: { type: 'string' },
55
- userVerdict: { type: 'string', enum: VERDICTS },
56
- },
57
- },
58
- };
59
- const updateFindingSchema = {
60
- ...findingIdParam,
61
- body: {
62
- type: 'object',
63
- additionalProperties: false,
64
- properties: {
65
- included: { type: 'boolean' },
66
- editedBody: { type: 'string' },
67
- },
68
- },
69
- };
70
- const postSchema = {
71
- ...reviewIdParam,
72
- body: {
73
- type: 'object',
74
- required: ['userVerdict'],
75
- additionalProperties: false,
76
- properties: { userVerdict: { type: 'string', enum: VERDICTS } },
77
- },
78
- };
79
- // Feature is opt-in (ENABLE_CLAUDE_REVIEW). When off, mutating routes 404.
80
- function featureOff(reply) {
81
- reply.status(404);
82
- return {
83
- error: 'NotFound',
84
- message: 'Claude Review is disabled (set ENABLE_CLAUDE_REVIEW=true).',
85
- };
86
- }
87
- export async function claudeReviewRoutes(app) {
88
- // Latest run + findings + history + auth + enabled.
89
- app.get('/api/prs/:id/claude-review', { schema: idParam }, async (req) => {
90
- const { id } = req.params;
91
- if (!config.claudeReviewEnabled) {
92
- return {
93
- enabled: false,
94
- auth: 'none',
95
- hasUserKey: false,
96
- review: null,
97
- history: [],
98
- };
99
- }
100
- const accountId = accountIdOf(req);
101
- const auth = detectClaudeAuth();
102
- return {
103
- enabled: true,
104
- auth: auth.status,
105
- authMessage: auth.status === 'none' ? auth.message : undefined,
106
- hasUserKey: hasUserAnthropicKey(),
107
- review: await getLatestClaudeReview(id, accountId),
108
- history: await listClaudeReviewHistory(id, accountId),
109
- };
110
- });
111
- // Set or clear the locally-stored Anthropic API key (local mode only — the
112
- // whole route file is unregistered in cloud). An empty `key` clears it.
113
- app.put('/api/claude-review/key', {
114
- schema: {
115
- body: {
116
- type: 'object',
117
- required: ['key'],
118
- additionalProperties: false,
119
- properties: { key: { type: 'string' } },
120
- },
121
- },
122
- }, async (req) => {
123
- const { key } = req.body;
124
- setUserAnthropicKey(key);
125
- return { hasUserKey: hasUserAnthropicKey(), auth: detectClaudeAuth().status };
126
- });
127
- // Kick off a run. 404 disabled/unknown PR, 400 no auth / no head, 409 busy.
128
- app.post('/api/prs/:id/claude-review', { schema: generateSchema }, async (req, reply) => {
129
- const { id } = req.params;
130
- const { model, mode } = req.body;
131
- if (!config.claudeReviewEnabled)
132
- return featureOff(reply);
133
- const auth = detectClaudeAuth();
134
- if (auth.status === 'none') {
135
- reply.status(400);
136
- return { error: 'NoClaudeAuth', message: auth.message };
137
- }
138
- const result = await startReview(id, model, mode ?? 'auto', app.log);
139
- if (!result.ok) {
140
- if (result.reason === 'not_found') {
141
- reply.status(404);
142
- return { error: 'NotFound', message: `PR ${id} not found` };
143
- }
144
- if (result.reason === 'no_head') {
145
- reply.status(400);
146
- return {
147
- error: 'NoHead',
148
- message: 'PR has no head commit to review yet.',
149
- };
150
- }
151
- if (result.reason === 'disabled')
152
- return featureOff(reply);
153
- // already_running | busy
154
- reply.status(409);
155
- return {
156
- error: 'Conflict',
157
- message: result.reason === 'already_running'
158
- ? 'A review is already running or queued for this PR.'
159
- : 'The review queue is full; try again once some finish.',
160
- };
161
- }
162
- reviewEvents.emit({
163
- type: 'review.requested',
164
- accountId: accountIdOf(req),
165
- prId: id,
166
- model,
167
- requestedMode: mode ?? 'auto',
168
- });
169
- reply.status(202);
170
- return { reviewId: result.reviewId, status: 'queued' };
171
- });
172
- // Live progress poll target (kept as a fallback + for the initial snapshot).
173
- app.get('/api/prs/:id/claude-review/status', { schema: idParam }, async (req) => {
174
- const { id } = req.params;
175
- if (!config.claudeReviewEnabled) {
176
- return { status: 'idle', reviewId: null, progress: null };
177
- }
178
- return await getReviewStatus(id);
179
- });
180
- // Live progress STREAM (SSE) — pushes each phase/activity/usage change in real
181
- // time and one terminal `done`, so the UI progress bar tracks the run without
182
- // polling. Local-only (Claude Review is gated off in cloud). We `hijack()` the
183
- // reply and own the raw socket for the connection's lifetime.
184
- app.get('/api/prs/:id/claude-review/stream', { schema: idParam }, async (req, reply) => {
185
- const { id } = req.params;
186
- reply.hijack();
187
- const raw = reply.raw;
188
- raw.writeHead(200, {
189
- 'Content-Type': 'text/event-stream',
190
- 'Cache-Control': 'no-cache, no-transform',
191
- Connection: 'keep-alive',
192
- // Disable proxy buffering (nginx) so frames flush immediately.
193
- 'X-Accel-Buffering': 'no',
194
- });
195
- const send = (data) => {
196
- if (!raw.writableEnded)
197
- raw.write(`data: ${JSON.stringify(data)}\n\n`);
198
- };
199
- if (!config.claudeReviewEnabled) {
200
- send({ type: 'done', status: 'idle', reviewId: null });
201
- raw.end();
202
- return;
203
- }
204
- let closed = false;
205
- let unsubscribe = null;
206
- const heartbeat = setInterval(() => {
207
- if (!raw.writableEnded)
208
- raw.write(': hb\n\n');
209
- }, 15000);
210
- const cleanup = () => {
211
- if (closed)
212
- return;
213
- closed = true;
214
- clearInterval(heartbeat);
215
- unsubscribe?.();
216
- if (!raw.writableEnded)
217
- raw.end();
218
- };
219
- // Subscribe BEFORE reading the snapshot so a terminal `done` emitted during
220
- // setup can't slip through the gap (it'd be delivered to this callback). If
221
- // the run already finished, the snapshot check below sends `done` itself.
222
- unsubscribe = subscribeReviewStream(id, (e) => {
223
- send(e);
224
- if (e.type === 'done')
225
- cleanup();
226
- });
227
- req.raw.on('close', cleanup);
228
- // Initial snapshot so a client that connects mid-run gets current state at once.
229
- const snap = await getReviewStatus(id);
230
- send({ type: 'snapshot', ...snap });
231
- if (snap.status !== 'running' && snap.status !== 'queued') {
232
- send({ type: 'done', status: snap.status, reviewId: snap.reviewId });
233
- cleanup();
234
- }
235
- });
236
- app.post('/api/prs/:id/claude-review/cancel', { schema: idParam }, async (req, reply) => {
237
- const { id } = req.params;
238
- if (!config.claudeReviewEnabled)
239
- return featureOff(reply);
240
- const ok = requestReviewCancel(id);
241
- if (!ok) {
242
- reply.status(404);
243
- return { error: 'NotFound', message: 'No running review for this PR.' };
244
- }
245
- return { status: 'cancelling' };
246
- });
247
- // Cross-PR list of prior Claude reviews (one entry per PR = its most-recent
248
- // succeeded run, within the timeline window). Static path — registered before
249
- // the /:reviewId param route so Fastify never mis-routes it.
250
- app.get('/api/claude-reviews', async (req) => {
251
- if (!config.claudeReviewEnabled)
252
- return { reviews: [] };
253
- return { reviews: await listAllClaudeReviews(accountIdOf(req)) };
254
- });
255
- // All in-flight reviews (global progress banner). Static path — Fastify
256
- // prioritises it over the /:reviewId param route.
257
- app.get('/api/claude-reviews/active', async () => {
258
- if (!config.claudeReviewEnabled)
259
- return { reviews: [] };
260
- return { reviews: await listActiveReviews() };
261
- });
262
- // A specific past run (with findings) — drives the history selector.
263
- app.get('/api/claude-reviews/:reviewId', { schema: reviewIdParam }, async (req, reply) => {
264
- if (!config.claudeReviewEnabled)
265
- return featureOff(reply);
266
- const { reviewId } = req.params;
267
- const review = await getClaudeReviewById(reviewId, accountIdOf(req));
268
- if (!review) {
269
- reply.status(404);
270
- return { error: 'NotFound', message: `Review ${reviewId} not found` };
271
- }
272
- return review;
273
- });
274
- // Save the user's authored draft (never touches Claude's summary/verdict).
275
- app.patch('/api/claude-reviews/:reviewId', { schema: updateReviewSchema }, async (req, reply) => {
276
- if (!config.claudeReviewEnabled)
277
- return featureOff(reply);
278
- const { reviewId } = req.params;
279
- const body = req.body;
280
- const ok = await updateReviewDraft(reviewId, body);
281
- if (!ok) {
282
- reply.status(404);
283
- return { error: 'NotFound', message: `Review ${reviewId} not found` };
284
- }
285
- reviewEvents.emit({
286
- type: 'review.draftUpdated',
287
- accountId: accountIdOf(req),
288
- reviewId,
289
- change: { userBody: body.userBody, userVerdict: body.userVerdict },
290
- });
291
- return { status: 'ok' };
292
- });
293
- // Tick a finding for inline posting and/or save the user's reworded body.
294
- app.patch('/api/claude-findings/:findingId', { schema: updateFindingSchema }, async (req, reply) => {
295
- if (!config.claudeReviewEnabled)
296
- return featureOff(reply);
297
- const { findingId } = req.params;
298
- const body = req.body;
299
- const ok = await updateFinding(findingId, body);
300
- if (!ok) {
301
- reply.status(404);
302
- return { error: 'NotFound', message: `Finding ${findingId} not found` };
303
- }
304
- reviewEvents.emit({
305
- type: 'finding.updated',
306
- accountId: accountIdOf(req),
307
- findingId,
308
- change: { included: body.included, editedBody: body.editedBody },
309
- });
310
- return { status: 'ok' };
311
- });
312
- // Post a single finding as a standalone comment (no review submitted). The
313
- // destination is chosen AUTOMATICALLY from the live diff: anchorable on its own
314
- // line → an inline comment there; unanchored but its file is in the diff → an
315
- // inline comment on the file's first change; its file is NOT in the diff (e.g. a
316
- // deep review on an unchanged file) → a standalone PR-level comment marked as
317
- // outside the PR's diff. Pins to the run's head SHA; 409 if the PR head has moved.
318
- app.post('/api/claude-findings/:findingId/post', { schema: findingIdParam }, async (req, reply) => {
319
- if (!config.claudeReviewEnabled)
320
- return featureOff(reply);
321
- const { findingId } = req.params;
322
- const accountId = accountIdOf(req);
323
- const ctx = await getFindingPostContext(findingId, accountId);
324
- if (!ctx) {
325
- reply.status(404);
326
- return { error: 'NotFound', message: `Finding ${findingId} not found` };
327
- }
328
- const f = ctx.finding;
329
- try {
330
- const currentHead = await fetchCurrentHeadSha(ctx.owner, ctx.name, ctx.prNumber);
331
- if (currentHead !== ctx.reviewHeadSha) {
332
- reply.status(409);
333
- return {
334
- error: 'HeadMoved',
335
- message: 'The PR head has moved since this review. Re-review before posting.',
336
- };
337
- }
338
- const postedAt = new Date().toISOString();
339
- // Anchorable on its own line → inline comment there.
340
- if (f.line != null && f.anchored) {
341
- const { commentId } = await submitGithubComment({
342
- owner: ctx.owner,
343
- name: ctx.name,
344
- prNumber: ctx.prNumber,
345
- commitId: ctx.reviewHeadSha,
346
- path: f.path,
347
- line: f.line,
348
- side: f.side,
349
- body: findingCommentBody({
350
- body: f.body,
351
- editedBody: f.editedBody,
352
- suggestion: f.suggestion,
353
- }),
354
- });
355
- await markFindingPosted(findingId, commentId, 'inline');
356
- reviewEvents.emit({
357
- type: 'finding.posted',
358
- accountId,
359
- findingId,
360
- postedCommentKind: 'inline',
361
- });
362
- const result = { githubCommentId: commentId, postedAt };
363
- return result;
364
- }
365
- // Otherwise consult the diff. If the file IS in the diff, re-anchor to its
366
- // first change (inline). If it isn't, post a standalone PR-level comment.
367
- const { diff } = stripNoiseFromDiff(await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber), isNoiseFile);
368
- const fb = fallbackAnchor(buildAnchorIndex(diff), f.path);
369
- if (fb) {
370
- const { commentId } = await submitGithubComment({
371
- owner: ctx.owner,
372
- name: ctx.name,
373
- prNumber: ctx.prNumber,
374
- commitId: ctx.reviewHeadSha,
375
- path: f.path,
376
- line: fb.line,
377
- side: fb.side,
378
- body: findingCommentBody({ body: f.body, editedBody: f.editedBody, suggestion: f.suggestion }, { fallbackNote: true }),
379
- });
380
- await markFindingPosted(findingId, commentId, 'inline');
381
- reviewEvents.emit({
382
- type: 'finding.posted',
383
- accountId,
384
- findingId,
385
- postedCommentKind: 'inline',
386
- });
387
- const result = { githubCommentId: commentId, postedAt };
388
- return result;
389
- }
390
- // File outside the PR's diff → standalone PR-level (issue) comment.
391
- const { commentId } = await submitGithubIssueComment({
392
- owner: ctx.owner,
393
- name: ctx.name,
394
- prNumber: ctx.prNumber,
395
- body: prLevelFindingBody({
396
- path: f.path,
397
- line: f.line,
398
- body: f.body,
399
- editedBody: f.editedBody,
400
- suggestion: f.suggestion,
401
- }),
402
- });
403
- await markFindingPosted(findingId, commentId, 'pr_comment');
404
- reviewEvents.emit({
405
- type: 'finding.posted',
406
- accountId,
407
- findingId,
408
- postedCommentKind: 'pr_comment',
409
- });
410
- const result = { githubCommentId: commentId, postedAt };
411
- return result;
412
- }
413
- catch (err) {
414
- reply.status(502);
415
- return {
416
- error: 'GitHubError',
417
- message: err instanceof Error ? err.message : String(err),
418
- };
419
- }
420
- });
421
- // Post a single GitHub review (or, with ?dryRun=true, return the exact payload
422
- // without calling GitHub).
423
- app.post('/api/claude-reviews/:reviewId/post', { schema: postSchema }, async (req, reply) => {
424
- if (!config.claudeReviewEnabled)
425
- return featureOff(reply);
426
- const { reviewId } = req.params;
427
- const { userVerdict } = req.body;
428
- const dryRun = req.query.dryRun === 'true';
429
- const accountId = accountIdOf(req);
430
- const ctx = await getClaudeReviewContext(reviewId, accountId);
431
- if (!ctx) {
432
- reply.status(404);
433
- return { error: 'NotFound', message: `Review ${reviewId} not found` };
434
- }
435
- const review = await getClaudeReviewById(reviewId, accountId);
436
- if (!review) {
437
- reply.status(404);
438
- return { error: 'NotFound', message: `Review ${reviewId} not found` };
439
- }
440
- try {
441
- // Re-validate against the live head — if it moved, a re-review is needed.
442
- const currentHead = await fetchCurrentHeadSha(ctx.owner, ctx.name, ctx.prNumber);
443
- if (currentHead !== ctx.review.headSha) {
444
- reply.status(409);
445
- return {
446
- error: 'HeadMoved',
447
- message: 'The PR head has moved since this review. Re-review before posting.',
448
- };
449
- }
450
- // Persist the chosen verdict so the run records what was posted.
451
- await updateReviewDraft(reviewId, { userVerdict });
452
- const rawDiff = await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber);
453
- const { diff } = stripNoiseFromDiff(rawDiff, isNoiseFile);
454
- const built = buildReview({
455
- commitId: ctx.review.headSha,
456
- body: review.userBody ?? '',
457
- event: userVerdict,
458
- includedFindings: review.findings.filter((f) => f.included),
459
- diff,
460
- });
461
- if (dryRun)
462
- return built.preview;
463
- const { reviewId: ghReviewId } = await submitGithubReview({
464
- owner: ctx.owner,
465
- name: ctx.name,
466
- prNumber: ctx.prNumber,
467
- commitId: ctx.review.headSha,
468
- body: review.userBody ?? '',
469
- event: userVerdict,
470
- comments: built.preview.comments,
471
- });
472
- // The review is now LIVE on GitHub and can't be un-posted. Findings whose
473
- // file isn't in the diff post as standalone PR-level comments alongside it
474
- // (one issue comment each), so a deep review's findings on unchanged files
475
- // still land rather than being dropped. Each post is best-effort: a single
476
- // failed comment must NOT strand the already-posted review (that would leave
477
- // the run unstamped and tempt a duplicate re-post), so we collect what lands
478
- // and ALWAYS stamp afterwards. Findings that fail simply stay un-posted and
479
- // can be posted individually from their row.
480
- const prCommentResults = [];
481
- for (const pc of built.preview.prComments) {
482
- try {
483
- const { commentId } = await submitGithubIssueComment({
484
- owner: ctx.owner,
485
- name: ctx.name,
486
- prNumber: ctx.prNumber,
487
- body: pc.body,
488
- });
489
- prCommentResults.push({ findingId: pc.findingId, commentId });
490
- }
491
- catch (err) {
492
- req.log.warn({ err, findingId: pc.findingId, path: pc.path }, 'failed to post a PR-level comment for an off-diff finding');
493
- }
494
- }
495
- await markReviewPosted(reviewId, ghReviewId, built.inlineFindingIds, prCommentResults);
496
- reviewEvents.emit({
497
- type: 'review.posted',
498
- accountId,
499
- reviewId,
500
- userVerdict,
501
- inlineFindingIds: built.inlineFindingIds,
502
- prCommentFindingIds: prCommentResults.map((r) => r.findingId),
503
- });
504
- const result = {
505
- postedReviewId: ghReviewId,
506
- postedAt: new Date().toISOString(),
507
- postedCommentCount: built.preview.comments.length,
508
- // Count what actually posted (a comment may have failed best-effort above).
509
- prCommentCount: prCommentResults.length,
510
- };
511
- return result;
512
- }
513
- catch (err) {
514
- reply.status(502);
515
- return {
516
- error: 'GitHubError',
517
- message: err instanceof Error ? err.message : String(err),
518
- };
519
- }
520
- });
521
- }
522
- //# sourceMappingURL=claude-review.js.map