pierre-review 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/claude-review.js +10 -2
- package/dist/api/routes/timeline.js +1 -1
- package/dist/app.js +33 -0
- package/dist/config.js +8 -2
- package/dist/db/queries.js +54 -0
- package/dist/db/schema.pg.js +1 -1
- package/dist/db/schema.sqlite.js +1 -1
- package/dist/review/agent.js +106 -5
- package/dist/review/clone-manager.js +22 -3
- package/dist/review/prompt.js +2 -1
- package/package.json +1 -1
- package/public/assets/index-Bfko6Dwb.js +1371 -0
- package/public/assets/index-DqMYNa1t.css +10 -0
- package/public/index.html +2 -2
- package/public/assets/index-C3UqK6P1.js +0 -1371
- package/public/assets/index-xfGfsqU7.css +0 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { config } from '../../config.js';
|
|
2
|
-
import { getClaudeReviewById, getClaudeReviewContext, getFindingPostContext, getLatestClaudeReview, listClaudeReviewHistory, } from '../../db/queries.js';
|
|
2
|
+
import { getClaudeReviewById, getClaudeReviewContext, getFindingPostContext, getLatestClaudeReview, listAllClaudeReviews, listClaudeReviewHistory, } from '../../db/queries.js';
|
|
3
3
|
import { markFindingPosted, markReviewPosted, updateFinding, updateReviewDraft, } from '../../review/persist.js';
|
|
4
4
|
import { getReviewStatus, listActiveReviews, requestReviewCancel, startReview, } from '../../review/review-manager.js';
|
|
5
5
|
import { detectClaudeAuth } from '../../review/auth.js';
|
|
@@ -7,7 +7,7 @@ import { hasUserAnthropicKey, setUserAnthropicKey, } from '../../review/local-se
|
|
|
7
7
|
import { buildReview, fetchCurrentHeadSha, fetchPrDiff, findingCommentBody, stripNoiseFromDiff, submitGithubComment, submitGithubReview, } from '../../review/post-review.js';
|
|
8
8
|
import { isNoiseFile } from '../../review/prompt.js';
|
|
9
9
|
import { accountIdOf } from '../plugins/auth.js';
|
|
10
|
-
const MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6'
|
|
10
|
+
const MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
11
11
|
const VERDICTS = ['COMMENT', 'REQUEST_CHANGES', 'APPROVE'];
|
|
12
12
|
const idParam = {
|
|
13
13
|
params: {
|
|
@@ -175,6 +175,14 @@ export async function claudeReviewRoutes(app) {
|
|
|
175
175
|
}
|
|
176
176
|
return { status: 'cancelling' };
|
|
177
177
|
});
|
|
178
|
+
// Cross-PR list of prior Claude reviews (one entry per PR = its most-recent
|
|
179
|
+
// succeeded run, within the timeline window). Static path — registered before
|
|
180
|
+
// the /:reviewId param route so Fastify never mis-routes it.
|
|
181
|
+
app.get('/api/claude-reviews', async (req) => {
|
|
182
|
+
if (!config.claudeReviewEnabled)
|
|
183
|
+
return { reviews: [] };
|
|
184
|
+
return { reviews: await listAllClaudeReviews(accountIdOf(req)) };
|
|
185
|
+
});
|
|
178
186
|
// All in-flight reviews (global progress banner). Static path — Fastify
|
|
179
187
|
// prioritises it over the /:reviewId param route.
|
|
180
188
|
app.get('/api/claude-reviews/active', async () => {
|
|
@@ -66,7 +66,7 @@ export async function timelineRoutes(app) {
|
|
|
66
66
|
userIds: parseIntList(q.userIds),
|
|
67
67
|
types: parseTypes(q.types),
|
|
68
68
|
statuses: parseStatuses(q.statuses),
|
|
69
|
-
excludeBots: q.excludeBots
|
|
69
|
+
excludeBots: q.excludeBots === 'true',
|
|
70
70
|
excludeStale: q.excludeStale === 'true',
|
|
71
71
|
};
|
|
72
72
|
return getTimeline(filters);
|
package/dist/app.js
CHANGED
|
@@ -30,6 +30,39 @@ export async function buildApp() {
|
|
|
30
30
|
: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss' } },
|
|
31
31
|
},
|
|
32
32
|
});
|
|
33
|
+
// Cloud only: canonicalise the host and pin HTTPS. Local mode runs on
|
|
34
|
+
// http://127.0.0.1, where HSTS would wrongly pin localhost to HTTPS and a www
|
|
35
|
+
// redirect is meaningless — so both are cloud-gated. Registered first so it runs
|
|
36
|
+
// before everything else (the redirect short-circuits before CORS/routing).
|
|
37
|
+
if (config.isCloud) {
|
|
38
|
+
let canonicalHost = '';
|
|
39
|
+
try {
|
|
40
|
+
// hostname (not host) so a configured port never enters the comparison.
|
|
41
|
+
canonicalHost = new URL(config.appBaseUrl).hostname.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
canonicalHost = '';
|
|
45
|
+
}
|
|
46
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
47
|
+
// HSTS: keep browsers on HTTPS for this domain. Honored only over HTTPS
|
|
48
|
+
// (Railway terminates TLS); ignored on plain HTTP. `includeSubDomains` also
|
|
49
|
+
// covers www. No `preload` — it's hard to undo. HSTS_MAX_AGE=0 disables it.
|
|
50
|
+
if (config.hstsMaxAge > 0) {
|
|
51
|
+
reply.header('Strict-Transport-Security', `max-age=${config.hstsMaxAge}; includeSubDomains`);
|
|
52
|
+
}
|
|
53
|
+
// Canonical host: 301 www.<apex> → <apex> so the OAuth round-trip and the
|
|
54
|
+
// session cookie stay on a single origin (and crawlers see one canonical
|
|
55
|
+
// URL). The HSTS header set above still rides on the redirect response.
|
|
56
|
+
if (canonicalHost) {
|
|
57
|
+
// Strip any :port from the Host header before comparing hostnames.
|
|
58
|
+
const host = (req.headers.host ?? '').toLowerCase().split(':')[0] ?? '';
|
|
59
|
+
if (host !== canonicalHost &&
|
|
60
|
+
host.replace(/^www\./, '') === canonicalHost) {
|
|
61
|
+
return reply.redirect(`${config.appBaseUrl}${req.url}`, 301);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
33
66
|
// CORS: in cloud mode lock to the app's own origin and allow credentials so the
|
|
34
67
|
// session cookie rides along; local mode reflects any origin (dev convenience).
|
|
35
68
|
await app.register(cors, {
|
package/dist/config.js
CHANGED
|
@@ -96,6 +96,11 @@ export const config = {
|
|
|
96
96
|
sessionSecret: process.env.SESSION_SECRET ?? '',
|
|
97
97
|
// 32-byte (64-hex) key for AES-256-GCM encryption of stored access tokens.
|
|
98
98
|
encryptionKey: process.env.ENCRYPTION_KEY ?? '',
|
|
99
|
+
// HSTS max-age (seconds) for the cloud public origin. Sent only in cloud mode
|
|
100
|
+
// and honored only over HTTPS (Railway terminates TLS). Default 1 year. Set
|
|
101
|
+
// HSTS_MAX_AGE=0 as a kill switch. `preload` is intentionally NOT sent — it is
|
|
102
|
+
// hard to undo; opt in manually once the domain is proven.
|
|
103
|
+
hstsMaxAge: intFromEnv('HSTS_MAX_AGE', 31536000),
|
|
99
104
|
// ---- Claude Review (agentic PR review; opt-in, LOCAL-ONLY) ----
|
|
100
105
|
// OFF by default: the feature spends real money / Agent-SDK credits per run.
|
|
101
106
|
// Enable with ENABLE_CLAUDE_REVIEW=true. FORCE-DISABLED in cloud mode (it
|
|
@@ -108,8 +113,9 @@ export const config = {
|
|
|
108
113
|
cloneCacheMaxBytes: intFromEnv('CLONE_CACHE_MAX_BYTES', 2 * 1024 * 1024 * 1024),
|
|
109
114
|
// Default model for the picker; per-run model still overrides on the request.
|
|
110
115
|
defaultReviewModel: process.env.DEFAULT_REVIEW_MODEL ?? 'claude-sonnet-4-6',
|
|
111
|
-
// Per-run caps (cost/disk/time runaway guards).
|
|
112
|
-
|
|
116
|
+
// Per-run caps (cost/disk/time runaway guards). The diff is inlined in full, so
|
|
117
|
+
// reviews need far fewer turns than the old default; 30 is still generous.
|
|
118
|
+
reviewMaxTurns: intFromEnv('REVIEW_MAX_TURNS', 30),
|
|
113
119
|
reviewBudgetUsd: floatFromEnv('REVIEW_BUDGET_USD', 1.0),
|
|
114
120
|
// At most one review per PR; this caps concurrent reviews across all PRs.
|
|
115
121
|
reviewConcurrency: intFromEnv('REVIEW_CONCURRENCY', 1),
|
package/dist/db/queries.js
CHANGED
|
@@ -1028,6 +1028,60 @@ export async function listClaudeReviewHistory(prId, accountId) {
|
|
|
1028
1028
|
finishedAt: iso(r.finishedAt),
|
|
1029
1029
|
}));
|
|
1030
1030
|
}
|
|
1031
|
+
// Cross-PR list of prior Claude reviews: ONE entry per PR = that PR's most-recent
|
|
1032
|
+
// SUCCEEDED run, accountId-scoped, restricted to PRs still within the timeline
|
|
1033
|
+
// window (open, or last touched within `backfillDays`), newest-first by finish
|
|
1034
|
+
// time. Used by GET /api/claude-reviews to populate the "prior reviews" view.
|
|
1035
|
+
export async function listAllClaudeReviews(accountId) {
|
|
1036
|
+
// Same window cutoff getTimeline uses for its overlap predicate (now − backfillDays).
|
|
1037
|
+
const cutoff = new Date(Date.now() - config.backfillDays * 24 * 60 * 60 * 1000);
|
|
1038
|
+
const rows = await db
|
|
1039
|
+
.select({
|
|
1040
|
+
reviewId: claudeReviews.id,
|
|
1041
|
+
prId: claudeReviews.prId,
|
|
1042
|
+
owner: repos.owner,
|
|
1043
|
+
name: repos.name,
|
|
1044
|
+
prNumber: pullRequests.number,
|
|
1045
|
+
prTitle: pullRequests.title,
|
|
1046
|
+
prState: pullRequests.state,
|
|
1047
|
+
summary: claudeReviews.summary,
|
|
1048
|
+
verdict: claudeReviews.verdict,
|
|
1049
|
+
headSha: claudeReviews.headSha,
|
|
1050
|
+
status: claudeReviews.status,
|
|
1051
|
+
createdAt: claudeReviews.createdAt,
|
|
1052
|
+
finishedAt: claudeReviews.finishedAt,
|
|
1053
|
+
})
|
|
1054
|
+
.from(claudeReviews)
|
|
1055
|
+
.innerJoin(pullRequests, eq(pullRequests.id, claudeReviews.prId))
|
|
1056
|
+
.innerJoin(repos, eq(repos.id, pullRequests.repoId))
|
|
1057
|
+
.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)))))
|
|
1058
|
+
.orderBy(desc(claudeReviews.finishedAt), desc(claudeReviews.createdAt))
|
|
1059
|
+
.execute();
|
|
1060
|
+
// Keep the first (most-recent) succeeded run per PR. N is small (single local
|
|
1061
|
+
// user), so a JS pass is simpler and portable across both dialects.
|
|
1062
|
+
const seen = new Set();
|
|
1063
|
+
const items = [];
|
|
1064
|
+
for (const r of rows) {
|
|
1065
|
+
if (seen.has(r.prId))
|
|
1066
|
+
continue;
|
|
1067
|
+
seen.add(r.prId);
|
|
1068
|
+
items.push({
|
|
1069
|
+
reviewId: r.reviewId,
|
|
1070
|
+
prId: r.prId,
|
|
1071
|
+
repoFullName: `${r.owner}/${r.name}`,
|
|
1072
|
+
prNumber: r.prNumber,
|
|
1073
|
+
prTitle: r.prTitle,
|
|
1074
|
+
prState: r.prState,
|
|
1075
|
+
summary: r.summary,
|
|
1076
|
+
verdict: r.verdict,
|
|
1077
|
+
headSha: r.headSha,
|
|
1078
|
+
status: r.status,
|
|
1079
|
+
createdAt: r.createdAt.toISOString(),
|
|
1080
|
+
finishedAt: iso(r.finishedAt),
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return items;
|
|
1084
|
+
}
|
|
1031
1085
|
export async function getFindingPostContext(findingId, accountId) {
|
|
1032
1086
|
const rows = await db
|
|
1033
1087
|
.select({
|
package/dist/db/schema.pg.js
CHANGED
|
@@ -311,7 +311,7 @@ export const claudeReviews = pgTable('claude_reviews', {
|
|
|
311
311
|
enum: ['queued', 'running', 'succeeded', 'failed', 'cancelled'],
|
|
312
312
|
}).notNull(),
|
|
313
313
|
model: text('model', {
|
|
314
|
-
enum: ['claude-opus-4-8', 'claude-sonnet-4-6'
|
|
314
|
+
enum: ['claude-opus-4-8', 'claude-sonnet-4-6'],
|
|
315
315
|
}).notNull(),
|
|
316
316
|
scope: text('scope', { enum: ['diff_only', 'worktree'] }),
|
|
317
317
|
summary: text('summary'),
|
package/dist/db/schema.sqlite.js
CHANGED
|
@@ -339,7 +339,7 @@ export const claudeReviews = sqliteTable('claude_reviews', {
|
|
|
339
339
|
enum: ['queued', 'running', 'succeeded', 'failed', 'cancelled'],
|
|
340
340
|
}).notNull(),
|
|
341
341
|
model: text('model', {
|
|
342
|
-
enum: ['claude-opus-4-8', 'claude-sonnet-4-6'
|
|
342
|
+
enum: ['claude-opus-4-8', 'claude-sonnet-4-6'],
|
|
343
343
|
}).notNull(),
|
|
344
344
|
// Null until the agent decides whether it explored the worktree.
|
|
345
345
|
scope: text('scope', { enum: ['diff_only', 'worktree'] }),
|
package/dist/review/agent.js
CHANGED
|
@@ -25,6 +25,8 @@ const DISALLOWED_TOOLS = [
|
|
|
25
25
|
'Bash(git commit *)',
|
|
26
26
|
'Bash(git config *)',
|
|
27
27
|
];
|
|
28
|
+
// How many recent-activity lines to keep in the live progress ring buffer.
|
|
29
|
+
const ACTIVITY_LOG_CAP = 25;
|
|
28
30
|
// Run a review end-to-end and persist its result. Owns status transitions:
|
|
29
31
|
// running → succeeded/failed/cancelled. Never throws to the caller for an
|
|
30
32
|
// expected failure (it records it); rethrows only truly unexpected errors so the
|
|
@@ -88,11 +90,30 @@ export async function runReview(args) {
|
|
|
88
90
|
abortController,
|
|
89
91
|
},
|
|
90
92
|
});
|
|
91
|
-
|
|
93
|
+
// Rolling, newest-last log of what the agent is doing right now, surfaced via
|
|
94
|
+
// onProgress (rides the /status poll). Live-only — never persisted.
|
|
95
|
+
const activity = [];
|
|
96
|
+
const pushActivity = (line) => {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed)
|
|
99
|
+
return;
|
|
100
|
+
activity.push(trimmed);
|
|
101
|
+
if (activity.length > ACTIVITY_LOG_CAP)
|
|
102
|
+
activity.shift();
|
|
103
|
+
};
|
|
92
104
|
for await (const message of q) {
|
|
93
|
-
if (message.type === 'assistant'
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
if (message.type === 'assistant') {
|
|
106
|
+
// Derive short, human-readable lines from the assistant turn's content
|
|
107
|
+
// blocks. Defensive throughout: content may be missing and tool input
|
|
108
|
+
// shapes vary — a malformed block must never abort the review.
|
|
109
|
+
try {
|
|
110
|
+
for (const line of describeAssistantBlocks(message))
|
|
111
|
+
pushActivity(line);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* never let progress derivation break the run */
|
|
115
|
+
}
|
|
116
|
+
onProgress({ phase: 'reviewing', recentActivity: [...activity] });
|
|
96
117
|
}
|
|
97
118
|
else if (message.type === 'result') {
|
|
98
119
|
result = message;
|
|
@@ -164,7 +185,17 @@ export async function runReview(args) {
|
|
|
164
185
|
if (repoCloneDir && worktreePath) {
|
|
165
186
|
await removeWorktree(repoCloneDir, worktreePath).catch(() => { });
|
|
166
187
|
}
|
|
167
|
-
|
|
188
|
+
// LRU eviction does a full recursive dir walk; keep it off the run-teardown
|
|
189
|
+
// critical path. Defer it (fire-and-forget) so the review result returns
|
|
190
|
+
// promptly. cleanupCloneCache is itself best-effort and never throws.
|
|
191
|
+
setImmediate(() => {
|
|
192
|
+
try {
|
|
193
|
+
cleanupCloneCache();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* advisory cleanup — never surface */
|
|
197
|
+
}
|
|
198
|
+
});
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
201
|
function errorMessage(err) {
|
|
@@ -172,4 +203,74 @@ function errorMessage(err) {
|
|
|
172
203
|
return err.message;
|
|
173
204
|
return String(err);
|
|
174
205
|
}
|
|
206
|
+
const TEXT_SNIPPET_CAP = 120;
|
|
207
|
+
const BASH_CMD_CAP = 80;
|
|
208
|
+
/**
|
|
209
|
+
* Turn one assistant message into a few short, human-readable progress lines:
|
|
210
|
+
* a label per tool_use block (e.g. `Read src/foo.ts`, `Grep "TODO"`, `Bash npm
|
|
211
|
+
* test`) plus a clipped snippet of any assistant text. Defensive throughout —
|
|
212
|
+
* the SDK content/tool-input shapes vary, so every access is guarded and this
|
|
213
|
+
* never throws.
|
|
214
|
+
*/
|
|
215
|
+
function describeAssistantBlocks(message) {
|
|
216
|
+
const lines = [];
|
|
217
|
+
const content = message?.message
|
|
218
|
+
?.content;
|
|
219
|
+
if (!Array.isArray(content))
|
|
220
|
+
return lines;
|
|
221
|
+
for (const raw of content) {
|
|
222
|
+
if (!raw || typeof raw !== 'object')
|
|
223
|
+
continue;
|
|
224
|
+
const block = raw;
|
|
225
|
+
if (block.type === 'tool_use') {
|
|
226
|
+
const name = typeof block.name === 'string' ? block.name : 'Tool';
|
|
227
|
+
const input = block.input && typeof block.input === 'object'
|
|
228
|
+
? block.input
|
|
229
|
+
: {};
|
|
230
|
+
lines.push(labelToolUse(name, input));
|
|
231
|
+
}
|
|
232
|
+
else if (block.type === 'text' && typeof block.text === 'string') {
|
|
233
|
+
const snippet = clip(block.text.replace(/\s+/g, ' ').trim(), TEXT_SNIPPET_CAP);
|
|
234
|
+
if (snippet)
|
|
235
|
+
lines.push(snippet);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
/** Build a short label for a tool call from its name + a truncated first arg. */
|
|
241
|
+
function labelToolUse(name, input) {
|
|
242
|
+
const str = (v) => typeof v === 'string' && v.trim().length > 0 ? v.trim() : null;
|
|
243
|
+
switch (name) {
|
|
244
|
+
case 'Read': {
|
|
245
|
+
const p = str(input.file_path) ?? str(input.path);
|
|
246
|
+
return p ? `Read ${p}` : 'Read …';
|
|
247
|
+
}
|
|
248
|
+
case 'Glob': {
|
|
249
|
+
const p = str(input.pattern);
|
|
250
|
+
return p ? `Glob ${p}` : 'Glob …';
|
|
251
|
+
}
|
|
252
|
+
case 'Grep': {
|
|
253
|
+
const p = str(input.pattern);
|
|
254
|
+
return p ? `Grep "${clip(p, BASH_CMD_CAP)}"` : 'Grep …';
|
|
255
|
+
}
|
|
256
|
+
case 'Bash': {
|
|
257
|
+
const c = str(input.command);
|
|
258
|
+
return c ? `Bash ${clip(c, BASH_CMD_CAP)}` : 'Bash …';
|
|
259
|
+
}
|
|
260
|
+
case 'mcp__review__submit_review':
|
|
261
|
+
return 'Submitting review…';
|
|
262
|
+
default: {
|
|
263
|
+
// Generic: name + a truncated first string-valued arg, if any.
|
|
264
|
+
for (const key of Object.keys(input)) {
|
|
265
|
+
const v = str(input[key]);
|
|
266
|
+
if (v)
|
|
267
|
+
return `${name} ${clip(v, BASH_CMD_CAP)}`;
|
|
268
|
+
}
|
|
269
|
+
return `${name} …`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function clip(s, max) {
|
|
274
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
275
|
+
}
|
|
175
276
|
//# sourceMappingURL=agent.js.map
|
|
@@ -46,11 +46,15 @@ export async function ensureClone(owner, name) {
|
|
|
46
46
|
* Fetch a PR's head ref into the clone's object store so its head commit
|
|
47
47
|
* (`sha`) becomes resolvable for a worktree checkout. The current PR head
|
|
48
48
|
* commit equals `pull/<n>/head`, so fetching that ref is sufficient.
|
|
49
|
+
*
|
|
50
|
+
* Fast path: if `sha` is already present in the local object store (a prior
|
|
51
|
+
* review of the same head left it there), skip the network fetch entirely —
|
|
52
|
+
* it's the dominant per-review network cost and is fully redundant when the
|
|
53
|
+
* head hasn't moved.
|
|
49
54
|
*/
|
|
50
55
|
export async function fetchPrHead(repoCloneDir, prNumber, sha) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
void sha;
|
|
56
|
+
if (await hasCommit(repoCloneDir, sha))
|
|
57
|
+
return;
|
|
54
58
|
await git([
|
|
55
59
|
'-C',
|
|
56
60
|
repoCloneDir,
|
|
@@ -61,6 +65,21 @@ export async function fetchPrHead(repoCloneDir, prNumber, sha) {
|
|
|
61
65
|
`pull/${prNumber}/head`,
|
|
62
66
|
]);
|
|
63
67
|
}
|
|
68
|
+
/** True if `sha` resolves to a commit object already in the local store. */
|
|
69
|
+
async function hasCommit(repoCloneDir, sha) {
|
|
70
|
+
if (!sha)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
await execFileAsync('git', ['-C', repoCloneDir, 'cat-file', '-e', `${sha}^{commit}`], {
|
|
74
|
+
timeout: 10_000,
|
|
75
|
+
maxBuffer: 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
64
83
|
/**
|
|
65
84
|
* Create an ephemeral detached worktree at `<clone>/.worktrees/<sha>` checked
|
|
66
85
|
* out at `sha`, and return its absolute path. If a stale worktree at that path
|
package/dist/review/prompt.js
CHANGED
|
@@ -113,6 +113,7 @@ Decide how far to look using this heuristic:
|
|
|
113
113
|
When in doubt about whether something exported is used elsewhere, look — a wrong signature change with stale callers is a blocker, not a nit.
|
|
114
114
|
|
|
115
115
|
# What you're given
|
|
116
|
+
- The COMPLETE unified diff for this PR is inlined in the user message. Use it directly — do NOT run \`git diff\`/\`git show\` to re-derive the change, and do NOT Read files just to see the diff. Explore the worktree only for context the diff doesn't show (callers, dependents, type definitions, surrounding code).
|
|
116
117
|
- Noise files (lockfiles and generated/vendored artifacts) have ALREADY been stripped from the diff. Do not ask for them and do not flag their absence.
|
|
117
118
|
- Line numbers for anchoring findings refer to the NEW-file (RIGHT) side of the diff unless you are pointing at a removed/old line.
|
|
118
119
|
|
|
@@ -194,7 +195,7 @@ export function buildUserPrompt(input) {
|
|
|
194
195
|
}
|
|
195
196
|
lines.push('## Diff');
|
|
196
197
|
lines.push('');
|
|
197
|
-
lines.push('
|
|
198
|
+
lines.push('The COMPLETE unified diff for this PR is provided in full below (noise files already removed). Use it directly — do NOT run `git diff` / `git show` to re-derive it, and do NOT Read files just to see the diff. Explore the worktree only for context the diff does not show: callers, dependents, type definitions, and surrounding code. Line numbers you cite for anchoring refer to the NEW-file (RIGHT) side unless you are pointing at a deleted line (LEFT).');
|
|
198
199
|
lines.push('');
|
|
199
200
|
lines.push('```diff');
|
|
200
201
|
lines.push(diff);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pierre-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Alex Wakeman",
|