claude-teammate 0.1.37 → 0.1.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/claude.js CHANGED
@@ -139,6 +139,34 @@ const GITHUB_PR_COMMENT_REVIEW_SCHEMA = {
139
139
  required: ["decision", "comment_body", "epic_facts", "epic_guardrails"]
140
140
  };
141
141
 
142
+ const GITHUB_PR_REVIEW_SCHEMA = {
143
+ type: "object",
144
+ additionalProperties: false,
145
+ properties: {
146
+ summary: {
147
+ type: "string"
148
+ },
149
+ suggestions: {
150
+ type: "array",
151
+ items: {
152
+ type: "object",
153
+ additionalProperties: false,
154
+ properties: {
155
+ file: { type: "string" },
156
+ line: { type: "number" },
157
+ side: { type: "string" },
158
+ body: { type: "string" },
159
+ classification: { type: "string", enum: ["Minor", "Major"] },
160
+ committable: { type: "boolean" },
161
+ suggestion: { type: "string" }
162
+ },
163
+ required: ["file", "line", "side", "body", "classification", "committable"]
164
+ }
165
+ }
166
+ },
167
+ required: ["summary", "suggestions"]
168
+ };
169
+
142
170
  const EPIC_MEMORY_CLEANUP_SCHEMA = {
143
171
  type: "object",
144
172
  additionalProperties: false,
@@ -502,6 +530,50 @@ export async function runClaudePullRequestCommentReview(input) {
502
530
  return validateGitHubPullRequestCommentReviewResult(parseClaudeOutput(stdout));
503
531
  }
504
532
 
533
+ export async function runClaudePrReview(input) {
534
+ if (!input.repoPath) {
535
+ throw new Error("PR review requires a local repository path.");
536
+ }
537
+
538
+ const repoPaths = [input.repoPath];
539
+ await Promise.all(repoPaths.map((repoPath) => clearLocalPlaywrightMcpConfig(repoPath)));
540
+
541
+ const args = [
542
+ "--print",
543
+ "--model",
544
+ input.model || DEFAULT_MODEL,
545
+ "--permission-mode",
546
+ "default",
547
+ "--strict-mcp-config",
548
+ "--tools",
549
+ "Read,Grep,Glob",
550
+ "--effort",
551
+ "medium",
552
+ "--output-format",
553
+ "json",
554
+ "--json-schema",
555
+ JSON.stringify(GITHUB_PR_REVIEW_SCHEMA),
556
+ "--append-system-prompt",
557
+ buildGitHubPRReviewSystemPrompt(),
558
+ buildGitHubPRReviewUserPrompt(input)
559
+ ];
560
+
561
+ let stdout;
562
+
563
+ try {
564
+ ({ stdout } = await runClaudeCommand("claude", args, {
565
+ cwd: input.repoPath,
566
+ maxBuffer: 10 * 1024 * 1024,
567
+ timeout: input.timeoutMs || DEFAULT_TIMEOUT_MS,
568
+ onSpawn: input.onSpawn
569
+ }));
570
+ } catch (error) {
571
+ throw new Error(formatClaudeInvocationError(error, input.timeoutMs || DEFAULT_TIMEOUT_MS));
572
+ }
573
+
574
+ return validateGitHubPrReviewResult(parseClaudeOutput(stdout));
575
+ }
576
+
505
577
  export function parseClaudeOutput(output) {
506
578
  const trimmed = output.trim();
507
579
  if (!trimmed) {
@@ -522,6 +594,10 @@ export function parseClaudeOutput(output) {
522
594
  return direct;
523
595
  }
524
596
 
597
+ if ("summary" in direct && "suggestions" in direct) {
598
+ return direct;
599
+ }
600
+
525
601
  if ("result" in direct && typeof direct.result === "string") {
526
602
  return JSON.parse(direct.result);
527
603
  }
@@ -634,6 +710,27 @@ function validateGitHubPullRequestCommentReviewResult(result) {
634
710
  };
635
711
  }
636
712
 
713
+ function validateGitHubPrReviewResult(result) {
714
+ if (!result || typeof result !== "object") {
715
+ throw new Error("Claude CLI returned an invalid GitHub PR review payload.");
716
+ }
717
+
718
+ return {
719
+ summary: String(result.summary ?? "").trim(),
720
+ suggestions: Array.isArray(result.suggestions)
721
+ ? result.suggestions.map((s) => ({
722
+ file: String(s.file ?? "").trim(),
723
+ line: Number(s.line) || 1,
724
+ side: String(s.side ?? "RIGHT").trim(),
725
+ body: String(s.body ?? "").trim(),
726
+ classification: ["Minor", "Major"].includes(s.classification) ? s.classification : "Minor",
727
+ committable: Boolean(s.committable),
728
+ suggestion: s.suggestion != null ? String(s.suggestion).trim() : undefined
729
+ }))
730
+ : []
731
+ };
732
+ }
733
+
637
734
  function validateEpicMemoryCleanupResult(result) {
638
735
  if (!result || typeof result !== "object") {
639
736
  throw new Error("Claude CLI returned an invalid epic memory cleanup payload.");
@@ -931,6 +1028,20 @@ function buildGitHubPRCommentReviewSystemPrompt() {
931
1028
  ].join(" ");
932
1029
  }
933
1030
 
1031
+ function buildGitHubPRReviewSystemPrompt() {
1032
+ return [
1033
+ "You are reviewing a GitHub pull request as a code reviewer.",
1034
+ "The primary input is the unified diff provided inline.",
1035
+ "You may also use Read, Grep, and Glob to read related code when the diff is ambiguous.",
1036
+ "Do not edit files, do not run mutating commands, and do not implement anything.",
1037
+ "Skip trivial style-only issues unless they introduce a real risk.",
1038
+ "Classify each suggestion as Minor (non-blocking) or Major (should be addressed before merge).",
1039
+ "Where possible, produce a committable suggestion with the corrected code.",
1040
+ "Return a summary paragraph describing what the PR does, followed by a summary of all suggestions.",
1041
+ "Return only structured output matching the provided schema."
1042
+ ].join(" ");
1043
+ }
1044
+
934
1045
  function buildEpicMemoryCleanupSystemPrompt() {
935
1046
  return [
936
1047
  "You are updating epic memory for an autonomous engineering workflow.",
@@ -1082,6 +1193,22 @@ Instructions:
1082
1193
  - If blocked, return result=stuck with that summary explaining why it did not work.`;
1083
1194
  }
1084
1195
 
1196
+ function buildGitHubPRReviewUserPrompt(input) {
1197
+ return `Review this GitHub pull request diff.
1198
+
1199
+ Pull request number: ${input.pr?.number ?? ""}
1200
+ Pull request title:
1201
+ ${input.pr?.title ?? ""}
1202
+
1203
+ Repository path:
1204
+ ${input.repoPath}
1205
+
1206
+ Unified diff:
1207
+ ${input.diff || "(empty)"}
1208
+
1209
+ Review the diff carefully. Read related code files with Read, Grep, or Glob only when the diff alone is insufficient to judge correctness. Return a summary describing the PR and its suggestions.`;
1210
+ }
1211
+
1085
1212
  function buildEpicMemoryCleanupUserPrompt(input) {
1086
1213
  return `Update this epic memory facts and guardrails and clean it up.
1087
1214
 
@@ -8,6 +8,7 @@ import {
8
8
  runClaudeEpicMemoryCleanup,
9
9
  runClaudeEpicMemorySummarize,
10
10
  runClaudeImplementation,
11
+ runClaudePrReview,
11
12
  runClaudePullRequestCommentReview,
12
13
  runClaudeGitHubIssueReview
13
14
  } from "../claude.js";
@@ -26,8 +27,10 @@ import {
26
27
  saveIssueMemory
27
28
  } from "../memory.js";
28
29
  import {
30
+ checkoutPullRequestBranch,
29
31
  commitAndPushRepoChanges,
30
32
  ensureBranchFromDefault,
33
+ ensureReviewRepo,
31
34
  hasUsableGitCheckout,
32
35
  listMissingRepoPaths,
33
36
  parseGitHubRepoUrl,
@@ -116,7 +119,12 @@ export async function runWorkerCommand({ projectRoot }) {
116
119
  draftPrCount: 0,
117
120
  draftPrs: [],
118
121
  prCommentReview: buildPrSubtaskState(previousState.prCommentReview),
119
- prImplementation: buildPrSubtaskState(previousState.prImplementation)
122
+ prImplementation: buildPrSubtaskState(previousState.prImplementation),
123
+ lastReviewPollAt: null,
124
+ lastReviewSuccessAt: null,
125
+ lastReviewError: null,
126
+ reviewPrCount: 0,
127
+ reviewPrs: []
120
128
  };
121
129
 
122
130
  const updatePrSubtaskState = async (key, updates) => {
@@ -136,6 +144,7 @@ export async function runWorkerCommand({ projectRoot }) {
136
144
  let jiraPolling = false;
137
145
  let githubPolling = false;
138
146
  let prPolling = false;
147
+ let reviewPolling = false;
139
148
 
140
149
  const shutdown = async (signal) => {
141
150
  if (stopping) {
@@ -344,9 +353,94 @@ export async function runWorkerCommand({ projectRoot }) {
344
353
  }
345
354
  };
346
355
 
356
+ const pollReviewPrs = async () => {
357
+ if (reviewPolling || stopping) {
358
+ return;
359
+ }
360
+
361
+ reviewPolling = true;
362
+ state.lastReviewPollAt = new Date().toISOString();
363
+
364
+ try {
365
+ const processedPrs = [];
366
+ const prs = await github.listPrsNeedingReview();
367
+ let reviewedPrCount = prs.length;
368
+
369
+ for (const pr of prs) {
370
+ if (!pr.repoUrl) {
371
+ await logger.error("PR review skipped because repository URL is missing", {
372
+ pr: pr.number,
373
+ title: pr.title
374
+ });
375
+ reviewedPrCount -= 1;
376
+ continue;
377
+ }
378
+
379
+ try {
380
+ const repoPath = await ensureReviewRepo(pr.repoUrl, runtimePaths.reviewReposDir);
381
+ const prDetail = await github.fetchPullRequest(pr.repoUrl, pr.number);
382
+ await checkoutPullRequestBranch(repoPath, prDetail.headRef, prDetail.headRepoCloneUrl);
383
+
384
+ const diff = await github.fetchPullRequestDiff(pr.repoUrl, pr.number);
385
+ const result = await runClaudePrReview({
386
+ diff,
387
+ repoPath,
388
+ pr: prDetail,
389
+ timeoutMs: parseOptionalInt(values.CLAUDE_TIMEOUT_MS)
390
+ });
391
+
392
+ await github.createPullRequestReview(
393
+ pr.repoUrl,
394
+ pr.number,
395
+ result.summary,
396
+ result.suggestions,
397
+ "COMMENT"
398
+ );
399
+
400
+ await github.addLabelsToPullRequest(pr.repoUrl, pr.number, ["AI-reviewed"]);
401
+
402
+ processedPrs.push({
403
+ repoUrl: pr.repoUrl,
404
+ pullRequestNumber: String(pr.number),
405
+ pullRequestUrl: prDetail.url,
406
+ suggestionsCount: result.suggestions.length
407
+ });
408
+
409
+ await logger.info("PR review submitted", {
410
+ repo: pr.repoUrl,
411
+ pr: pr.number,
412
+ suggestions: result.suggestions.length
413
+ });
414
+ } catch (error) {
415
+ await logger.error("PR review failed", {
416
+ repo: pr.repoUrl,
417
+ pr: pr.number,
418
+ error
419
+ });
420
+ }
421
+ }
422
+
423
+ state.lastReviewSuccessAt = new Date().toISOString();
424
+ state.lastReviewError = null;
425
+ state.reviewPrCount = reviewedPrCount;
426
+ state.reviewPrs = processedPrs.slice(0, 20);
427
+ await writeState(runtimePaths.stateFile, state);
428
+ await logger.info("PR review poll complete", {
429
+ reviewed: reviewedPrCount
430
+ });
431
+ } catch (error) {
432
+ state.lastReviewError = error instanceof Error ? error.message : String(error);
433
+ await writeState(runtimePaths.stateFile, state);
434
+ await logger.error("PR review poll failed", { error });
435
+ } finally {
436
+ reviewPolling = false;
437
+ }
438
+ };
439
+
347
440
  await pollJira();
348
441
  await pollGitHub();
349
442
  await pollDraftPrs();
443
+ await pollReviewPrs();
350
444
  setInterval(() => {
351
445
  void pollJira();
352
446
  }, POLL_INTERVAL_MS);
@@ -356,6 +450,9 @@ export async function runWorkerCommand({ projectRoot }) {
356
450
  setInterval(() => {
357
451
  void pollDraftPrs();
358
452
  }, POLL_INTERVAL_MS);
453
+ setInterval(() => {
454
+ void pollReviewPrs();
455
+ }, POLL_INTERVAL_MS);
359
456
  }
360
457
 
361
458
  async function processJiraIssue({ issue, jira, github, botUser, config, projectRoot, runtimePaths, logger }) {
package/src/github.js CHANGED
@@ -290,6 +290,85 @@ export function createGitHubClient(config) {
290
290
  return payload.default_branch;
291
291
  },
292
292
 
293
+ async listPrsNeedingReview() {
294
+ const query = "is:pr+is:open+review-requested:@me+-label:AI-reviewed";
295
+ const url = new URL("https://api.github.com/search/issues");
296
+ url.searchParams.set("q", query);
297
+ url.searchParams.set("per_page", "10");
298
+
299
+ const payload = await requestGitHub(url, config, { method: "GET" });
300
+ return Array.isArray(payload.items)
301
+ ? payload.items.map((item) => ({
302
+ repoUrl: mapRepositoryApiUrlToHtmlUrl(item.repository_url),
303
+ number: item.number,
304
+ title: item.title ?? ""
305
+ }))
306
+ : [];
307
+ },
308
+
309
+ async addLabelsToPullRequest(repoUrl, prNumber, labels) {
310
+ const repo = parseGitHubRepoUrl(repoUrl);
311
+ return requestGitHub(
312
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${prNumber}/labels`,
313
+ config,
314
+ {
315
+ method: "POST",
316
+ body: JSON.stringify({ labels })
317
+ },
318
+ repo
319
+ );
320
+ },
321
+
322
+ async createPullRequestReview(repoUrl, prNumber, body, suggestions, event) {
323
+ const repo = parseGitHubRepoUrl(repoUrl);
324
+ const comments = Array.isArray(suggestions)
325
+ ? suggestions.map((s) => {
326
+ let commentBody = String(s.body || "").trim();
327
+ if (s.committable && s.suggestion) {
328
+ commentBody = `${commentBody}\n\n\`\`\`suggestion\n${s.suggestion}\n\`\`\``.trim();
329
+ }
330
+ return {
331
+ path: String(s.file || ""),
332
+ line: Number(s.line) || 1,
333
+ side: String(s.side || "RIGHT").toUpperCase(),
334
+ body: commentBody
335
+ };
336
+ })
337
+ : [];
338
+
339
+ return requestGitHub(
340
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/reviews`,
341
+ config,
342
+ {
343
+ method: "POST",
344
+ body: JSON.stringify({ body: String(body || "").trim(), event: event || "COMMENT", comments })
345
+ },
346
+ repo
347
+ );
348
+ },
349
+
350
+ async fetchPullRequestDiff(repoUrl, prNumber) {
351
+ const repo = parseGitHubRepoUrl(repoUrl);
352
+ const response = await fetch(
353
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`,
354
+ {
355
+ headers: {
356
+ Accept: "application/vnd.github.v3.diff",
357
+ Authorization: `Bearer ${config.GITHUB_PAT}`,
358
+ "User-Agent": "claude-teammate"
359
+ }
360
+ }
361
+ );
362
+ if (!response.ok) {
363
+ const errorBody = await response.text();
364
+ throw new Error(buildGitHubRequestError(response.status, repo, errorBody, {
365
+ method: "GET",
366
+ path: `/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`
367
+ }));
368
+ }
369
+ return response.text();
370
+ },
371
+
293
372
  async createPullRequest(repoUrl, pullRequest) {
294
373
  const repo = parseGitHubRepoUrl(repoUrl);
295
374
  const payload = await requestGitHub(
@@ -316,6 +395,21 @@ export function createGitHubClient(config) {
316
395
  };
317
396
  }
318
397
 
398
+ function mapRepositoryApiUrlToHtmlUrl(repositoryApiUrl) {
399
+ const value = String(repositoryApiUrl || "").trim();
400
+
401
+ if (!value) {
402
+ return "";
403
+ }
404
+
405
+ const match = value.match(/^https:\/\/api\.github\.com\/repos\/([^/]+)\/([^/]+)$/u);
406
+ if (!match) {
407
+ return value;
408
+ }
409
+
410
+ return `https://github.com/${match[1]}/${match[2]}`;
411
+ }
412
+
319
413
  function mapGitHubIssue(payload, comments = []) {
320
414
  return {
321
415
  number: payload.number,
@@ -347,7 +441,8 @@ function mapGitHubPullRequestSummary(payload) {
347
441
  id: payload.user?.id ?? null
348
442
  },
349
443
  headRef: payload.head?.ref ?? "",
350
- baseRef: payload.base?.ref ?? ""
444
+ baseRef: payload.base?.ref ?? "",
445
+ headRepoCloneUrl: payload.head?.repo?.clone_url ?? null
351
446
  };
352
447
  }
353
448
 
package/src/repo.js CHANGED
@@ -54,6 +54,66 @@ export async function validateOrEnsureLocalRepo(repoUrl, reposDir, localPath = "
54
54
  return ensureLocalRepo(repoUrl, reposDir);
55
55
  }
56
56
 
57
+ export async function ensureReviewRepo(repoUrl, reviewReposDir) {
58
+ const repo = parseGitHubRepoUrl(repoUrl);
59
+ const checkoutPath = path.join(reviewReposDir, repo.owner, repo.name);
60
+
61
+ if (await isUsableGitCheckout(checkoutPath)) {
62
+ await execGit(checkoutPath, ["checkout", "--", "."]);
63
+ await execGit(checkoutPath, ["clean", "-fd"]);
64
+ return checkoutPath;
65
+ }
66
+
67
+ await mkdir(path.dirname(checkoutPath), { recursive: true });
68
+ await execFileAsync("git", ["clone", repo.cloneUrl, checkoutPath], {
69
+ maxBuffer: 10 * 1024 * 1024,
70
+ env: buildGitEnv(repo.cloneUrl)
71
+ });
72
+
73
+ return checkoutPath;
74
+ }
75
+
76
+ export async function checkoutPullRequestBranch(repoPath, branchName, headRepoCloneUrl = null) {
77
+ const originUrl = await execGitOutput(repoPath, ["remote", "get-url", "origin"]);
78
+ const normalizedOrigin = originUrl.trim().replace(/\.git$/u, "").toLowerCase();
79
+ const normalizedHead = headRepoCloneUrl
80
+ ? headRepoCloneUrl.trim().replace(/\.git$/u, "").toLowerCase()
81
+ : null;
82
+
83
+ const isFork = normalizedHead && normalizedHead !== normalizedOrigin;
84
+
85
+ if (isFork) {
86
+ const remoteName = "pr-head";
87
+ let remoteExists = false;
88
+ try {
89
+ await execGitOutput(repoPath, ["remote", "get-url", remoteName]);
90
+ remoteExists = true;
91
+ } catch {
92
+ remoteExists = false;
93
+ }
94
+
95
+ if (remoteExists) {
96
+ await execGit(repoPath, ["remote", "set-url", remoteName, headRepoCloneUrl]);
97
+ } else {
98
+ await execFileAsync("git", ["remote", "add", remoteName, headRepoCloneUrl], {
99
+ cwd: repoPath,
100
+ maxBuffer: 10 * 1024 * 1024,
101
+ env: buildGitEnv(headRepoCloneUrl)
102
+ });
103
+ }
104
+
105
+ await execFileAsync("git", ["fetch", remoteName], {
106
+ cwd: repoPath,
107
+ maxBuffer: 10 * 1024 * 1024,
108
+ env: buildGitEnv(headRepoCloneUrl)
109
+ });
110
+ await execGit(repoPath, ["checkout", "-B", branchName, `${remoteName}/${branchName}`]);
111
+ } else {
112
+ await execGit(repoPath, ["fetch", "origin"]);
113
+ await execGit(repoPath, ["checkout", "-B", branchName, `origin/${branchName}`]);
114
+ }
115
+ }
116
+
57
117
  export async function ensureBranchFromDefault(repoPath, branchName, baseBranch) {
58
118
  try {
59
119
  await execGit(repoPath, ["fetch", "origin", baseBranch, "--prune"]);
package/src/runtime.js CHANGED
@@ -11,7 +11,8 @@ export function getRuntimePaths(projectRoot) {
11
11
  pidFile: path.join(runtimeDir, "worker.pid"),
12
12
  stateFile: path.join(runtimeDir, "state.json"),
13
13
  logFile: path.join(runtimeDir, "worker.log"),
14
- reposDir: path.join(runtimeDir, "repos")
14
+ reposDir: path.join(runtimeDir, "repos"),
15
+ reviewReposDir: path.join(runtimeDir, "review-repos")
15
16
  };
16
17
  }
17
18