@warpmetrics/coder 0.2.9 → 0.2.10

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": "@warpmetrics/coder",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "Local agent loop for implementing GitHub issues with Claude Code. Powered by WarpMetrics.",
6
6
  "bin": {
package/src/git.js CHANGED
@@ -17,6 +17,10 @@ export function createBranch(dir, name) {
17
17
  run(`git checkout -b ${name}`, { cwd: dir });
18
18
  }
19
19
 
20
+ export function getHead(dir) {
21
+ return run(`git rev-parse HEAD`, { cwd: dir });
22
+ }
23
+
20
24
  export function hasNewCommits(dir, base = 'main') {
21
25
  const log = run(`git log ${base}..HEAD --oneline`, { cwd: dir });
22
26
  return log.length > 0;
@@ -59,6 +63,10 @@ export function getReviewComments(prNumber, { repo }) {
59
63
  return JSON.parse(out);
60
64
  }
61
65
 
66
+ export function dismissReview(prNumber, reviewId, { repo, message }) {
67
+ run(`gh api repos/${repo}/pulls/${prNumber}/reviews/${reviewId}/dismissals -X PUT -f message=${JSON.stringify(message)}`);
68
+ }
69
+
62
70
  export function updatePRBody(prNumber, { repo, body }) {
63
71
  run(`gh pr edit ${prNumber} --repo ${repo} --body ${JSON.stringify(body)}`);
64
72
  }
package/src/revise.js CHANGED
@@ -158,6 +158,8 @@ export async function revise(item, { board, config, log, refActId, since }) {
158
158
 
159
159
  const prompt = promptParts.join('\n');
160
160
 
161
+ const headBefore = git.getHead(workdir);
162
+
161
163
  log(' running claude...');
162
164
  claudeResult = await claude.run({
163
165
  prompt,
@@ -182,32 +184,61 @@ export async function revise(item, { board, config, log, refActId, since }) {
182
184
  git.commitAll(workdir, 'Address review feedback');
183
185
  }
184
186
 
185
- // Push
186
- log(' pushing...');
187
- git.push(workdir, branch);
187
+ // Check if Claude actually made changes
188
+ const headAfter = git.getHead(workdir);
189
+ if (headAfter === headBefore) {
190
+ log(' no changes needed — review feedback already addressed');
188
191
 
189
- // Update PR body with new act ID for next review cycle
190
- if (actId) {
192
+ // Dismiss active CHANGES_REQUESTED reviews (they're stale)
191
193
  try {
192
- let body = git.getPRBody(prNumber, { repo: repoName });
193
- body = body.replace(/<!-- wm:act:wm_act_\w+ -->/, `<!-- wm:act:${actId} -->`);
194
- if (!body.includes(`<!-- wm:act:${actId} -->`)) {
195
- body += `\n\n<!-- wm:act:${actId} -->`;
194
+ const reviews = git.getReviews(prNumber, { repo: repoName });
195
+ for (const r of reviews) {
196
+ if (r.state === 'CHANGES_REQUESTED') {
197
+ git.dismissReview(prNumber, r.id, {
198
+ repo: repoName,
199
+ message: 'Code verified correct by warp-coder — no changes needed.',
200
+ });
201
+ log(` dismissed stale review ${r.id}`);
202
+ }
196
203
  }
197
- git.updatePRBody(prNumber, { repo: repoName, body });
198
204
  } catch (err) {
199
- log(` warning: could not update PR body with act ID: ${err.message}`);
205
+ log(` warning: could not dismiss stale reviews: ${err.message}`);
200
206
  }
201
- }
202
207
 
203
- // Move back to In Review
204
- try {
205
- await board.moveToReview(item);
206
- } catch (err) {
207
- log(` warning: could not move to In Review: ${err.message}`);
208
- }
208
+ // Move back to In Review (no active CHANGES_REQUESTED, so won't be picked up again)
209
+ try {
210
+ await board.moveToReview(item);
211
+ } catch (err) {
212
+ log(` warning: could not move to In Review: ${err.message}`);
213
+ }
214
+ success = true;
215
+ } else {
216
+ // Push
217
+ log(' pushing...');
218
+ git.push(workdir, branch);
219
+
220
+ // Update PR body with new act ID for next review cycle
221
+ if (actId) {
222
+ try {
223
+ let body = git.getPRBody(prNumber, { repo: repoName });
224
+ body = body.replace(/<!-- wm:act:wm_act_\w+ -->/, `<!-- wm:act:${actId} -->`);
225
+ if (!body.includes(`<!-- wm:act:${actId} -->`)) {
226
+ body += `\n\n<!-- wm:act:${actId} -->`;
227
+ }
228
+ git.updatePRBody(prNumber, { repo: repoName, body });
229
+ } catch (err) {
230
+ log(` warning: could not update PR body with act ID: ${err.message}`);
231
+ }
232
+ }
209
233
 
210
- success = true;
234
+ // Move back to In Review
235
+ try {
236
+ await board.moveToReview(item);
237
+ } catch (err) {
238
+ log(` warning: could not move to In Review: ${err.message}`);
239
+ }
240
+ success = true;
241
+ }
211
242
  } catch (err) {
212
243
  taskError = err.message;
213
244
  log(` failed: ${err.message}`);
package/src/warp.js CHANGED
@@ -204,7 +204,7 @@ export async function findIssueRun(apiKey, { repo, issueNumber }) {
204
204
  const lastOutcome = outcomes[outcomes.length - 1];
205
205
  const blockedAt = lastOutcome?.name === 'Max Retries' ? lastOutcome.timestamp : null;
206
206
 
207
- return { runId: match.id, blockedAt };
207
+ return { runId: match.id, blockedAt, countSince: null };
208
208
  }
209
209
 
210
210
  export async function countRevisions(apiKey, { prNumber, repo, since }) {
package/src/watch.js CHANGED
@@ -58,7 +58,7 @@ export async function watch() {
58
58
  const issue = await warp.createIssueRun(config.warpmetricsApiKey, {
59
59
  repo: repoName, issueNumber, issueTitle,
60
60
  });
61
- issueRuns.set(issueNumber, { runId: issue.runId, blockedAt: null });
61
+ issueRuns.set(issueNumber, { runId: issue.runId, blockedAt: null, countSince: null });
62
62
  implementActId = issue.actId;
63
63
  log(` issue run: ${issue.runId}`);
64
64
  } catch (err) {
@@ -92,11 +92,10 @@ export async function watch() {
92
92
  }
93
93
 
94
94
  // Detect resume: item was previously blocked, human moved it back to In Review
95
- let since = null;
96
95
  if (issueCtx?.blockedAt) {
97
- since = issueCtx.blockedAt;
96
+ issueCtx.countSince = issueCtx.blockedAt;
98
97
  issueCtx.blockedAt = null;
99
- log(` resumed (counter reset)`);
98
+ log(` resumed from blocked (counter reset, since: ${issueCtx.countSince})`);
100
99
  if (config.warpmetricsApiKey) {
101
100
  try {
102
101
  await warp.closeIssueRun(config.warpmetricsApiKey, {
@@ -106,12 +105,13 @@ export async function watch() {
106
105
  }
107
106
  }
108
107
 
109
- const result = await revise(item, { board, config, log, refActId: item._reviewActId, since });
108
+ const result = await revise(item, { board, config, log, refActId: item._reviewActId, since: issueCtx?.countSince });
110
109
 
111
110
  // Record outcome on the issue run if revision failed terminally
112
111
  if (!result.success && issueCtx && config.warpmetricsApiKey) {
113
112
  const name = result.reason === 'max_retries' ? 'Max Retries' : 'Revision Failed';
114
113
  issueCtx.blockedAt = new Date().toISOString();
114
+ issueCtx.countSince = null;
115
115
  try {
116
116
  await warp.closeIssueRun(config.warpmetricsApiKey, {
117
117
  runId: issueCtx.runId,