pr-shepherd 0.5.1 → 0.6.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
4
- "version": "0.5.1",
4
+ "version": "0.6.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -16,5 +16,5 @@
16
16
  "code-review",
17
17
  "automation"
18
18
  ],
19
- "skills": "./skills/"
19
+ "skills": "./plugin/skills/"
20
20
  }
@@ -85,7 +85,7 @@ export async function runCheck(opts) {
85
85
  !verdict.anyInProgress &&
86
86
  verdict.filteredNames.length > 0;
87
87
  // Compute overall status.
88
- const status = computeStatus(verdict, actionableThreads.length, actionableComments.length, mergeStatus.status, batchData.changesRequestedReviews.length);
88
+ const status = computeStatus(verdict, actionableThreads.length, actionableComments.length, mergeStatus, batchData.changesRequestedReviews.length);
89
89
  return {
90
90
  pr: prNumber,
91
91
  repo: `${repo.owner}/${repo.name}`,
@@ -117,7 +117,7 @@ export async function runCheck(opts) {
117
117
  // ---------------------------------------------------------------------------
118
118
  function computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedReviews) {
119
119
  // Merge conflicts are always terminal regardless of CI state.
120
- if (mergeStatus === "CONFLICTS")
120
+ if (mergeStatus.status === "CONFLICTS")
121
121
  return "FAILING";
122
122
  // Check CI state before merge-blocking states: BLOCKED/UNSTABLE/BEHIND are
123
123
  // often caused by CI not having passed yet, so they shouldn't mask IN_PROGRESS.
@@ -125,16 +125,29 @@ function computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStat
125
125
  return "FAILING";
126
126
  if (verdict.anyInProgress)
127
127
  return "IN_PROGRESS";
128
- if (mergeStatus === "BLOCKED" || mergeStatus === "UNSTABLE" || mergeStatus === "BEHIND")
128
+ // BLOCKED solely because a human reviewer hasn't approved yet shepherd is done, hand off.
129
+ // copilotReviewInProgress means a bot still owes a review, which is not this case.
130
+ if (verdict.allPassed &&
131
+ unresolvedThreads === 0 &&
132
+ unresolvedComments === 0 &&
133
+ changesRequestedReviews === 0 &&
134
+ mergeStatus.status === "BLOCKED" &&
135
+ !mergeStatus.copilotReviewInProgress &&
136
+ mergeStatus.reviewDecision === "REVIEW_REQUIRED") {
137
+ return "READY";
138
+ }
139
+ if (mergeStatus.status === "BLOCKED" ||
140
+ mergeStatus.status === "UNSTABLE" ||
141
+ mergeStatus.status === "BEHIND")
129
142
  return "FAILING";
130
- if (mergeStatus === "UNKNOWN")
143
+ if (mergeStatus.status === "UNKNOWN")
131
144
  return "UNKNOWN";
132
145
  if (changesRequestedReviews > 0)
133
146
  return "UNRESOLVED_COMMENTS";
134
147
  if (unresolvedThreads > 0 || unresolvedComments > 0)
135
148
  return "UNRESOLVED_COMMENTS";
136
149
  // DRAFT is treated the same as CLEAN for readiness — marking the PR ready resolves it.
137
- if ((mergeStatus === "CLEAN" || mergeStatus === "DRAFT") && verdict.allPassed)
150
+ if ((mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT") && verdict.allPassed)
138
151
  return "READY";
139
152
  return "UNKNOWN";
140
153
  }
@@ -40,7 +40,7 @@ export async function runResolveFetch(opts) {
40
40
  }
41
41
  const activeThreads = unresolvedThreads.filter((t) => !t.isOutdated);
42
42
  return {
43
- actionableThreads: activeThreads.map(({ isResolved, isOutdated, ...rest }) => rest),
43
+ actionableThreads: activeThreads.map(({ isResolved: _r, isOutdated: _o, ...rest }) => rest),
44
44
  actionableComments: visibleComments,
45
45
  changesRequestedReviews: data.changesRequestedReviews,
46
46
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -39,6 +39,14 @@ Invoke `/loop <INTERVAL> --max-turns 50 --expires 8h` via the Skill tool. Use th
39
39
 
40
40
  ````
41
41
  # pr-shepherd-loop:pr=<PR_NUMBER>
42
+
43
+ **IMPORTANT — recurrence rules for this session:**
44
+ - **Do NOT call `ScheduleWakeup`.** This session was fired by a recurring cron job. Calling `ScheduleWakeup` (with a `/loop` prompt) would create a duplicate cron job, leading to concurrent git operations and `.git/index.lock` collisions.
45
+ - **Do NOT invoke `/loop`.** Same reason — `/loop` with an interval calls `CronCreate`, which creates a second recurrent runner.
46
+ - After completing the actions below, end the turn cleanly. The cron job handles the next fire automatically.
47
+
48
+ **Self-dedup:** Run `CronList`. If more than one job has a prompt containing `# pr-shepherd-loop:pr=<PR_NUMBER>`, duplicate runners exist. Keep the job with the lowest job ID and `CronDelete` the rest (ignore errors if a job is already gone — another concurrent runner may have deleted it first), then continue this iteration.
49
+
42
50
  Run the following in a single Bash invocation:
43
51
  npx pr-shepherd iterate <PR_NUMBER> --ready-delay <READY_DELAY> --no-cache --last-push-time "$(git log -1 --format=%ct HEAD)" --format=json
44
52