@warpmetrics/coder 0.2.8 → 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 +1 -1
- package/src/git.js +8 -0
- package/src/revise.js +50 -19
- package/src/warp.js +21 -0
- package/src/watch.js +20 -6
package/package.json
CHANGED
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
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
if (actId) {
|
|
192
|
+
// Dismiss active CHANGES_REQUESTED reviews (they're stale)
|
|
191
193
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
205
|
+
log(` warning: could not dismiss stale reviews: ${err.message}`);
|
|
200
206
|
}
|
|
201
|
-
}
|
|
202
207
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
@@ -186,6 +186,27 @@ export async function emitAct(apiKey, { outcomeId, actId, name, opts }) {
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export async function findIssueRun(apiKey, { repo, issueNumber }) {
|
|
190
|
+
const runs = await findRuns(apiKey, 'issue');
|
|
191
|
+
const match = runs.find(r =>
|
|
192
|
+
r.opts?.repo === repo &&
|
|
193
|
+
r.opts?.issue === String(issueNumber)
|
|
194
|
+
);
|
|
195
|
+
if (!match) return null;
|
|
196
|
+
|
|
197
|
+
const res = await fetch(`${API_URL}/v1/runs/${match.id}`, {
|
|
198
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) return null;
|
|
201
|
+
const { data } = await res.json();
|
|
202
|
+
|
|
203
|
+
const outcomes = data.outcomes || [];
|
|
204
|
+
const lastOutcome = outcomes[outcomes.length - 1];
|
|
205
|
+
const blockedAt = lastOutcome?.name === 'Max Retries' ? lastOutcome.timestamp : null;
|
|
206
|
+
|
|
207
|
+
return { runId: match.id, blockedAt, countSince: null };
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
export async function countRevisions(apiKey, { prNumber, repo, since }) {
|
|
190
211
|
try {
|
|
191
212
|
const runs = await findRuns(apiKey, 'agent-pipeline');
|
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) {
|
|
@@ -74,15 +74,28 @@ export async function watch() {
|
|
|
74
74
|
for (const item of reviewItems) {
|
|
75
75
|
if (!running) break;
|
|
76
76
|
const issueNumber = item.content?.number;
|
|
77
|
-
|
|
77
|
+
let issueCtx = issueNumber ? issueRuns.get(issueNumber) : null;
|
|
78
78
|
log(`Found review feedback: PR #${item._prNumber || item.content?.number}`);
|
|
79
79
|
|
|
80
|
+
// Recover issue run from WM if watcher was restarted
|
|
81
|
+
if (!issueCtx && issueNumber && config.warpmetricsApiKey) {
|
|
82
|
+
try {
|
|
83
|
+
const recovered = await warp.findIssueRun(config.warpmetricsApiKey, { repo: repoName, issueNumber });
|
|
84
|
+
if (recovered) {
|
|
85
|
+
issueRuns.set(issueNumber, recovered);
|
|
86
|
+
issueCtx = recovered;
|
|
87
|
+
log(` recovered issue run: ${recovered.runId}`);
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log(` warning: could not recover issue run: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
// Detect resume: item was previously blocked, human moved it back to In Review
|
|
81
|
-
let since = null;
|
|
82
95
|
if (issueCtx?.blockedAt) {
|
|
83
|
-
|
|
96
|
+
issueCtx.countSince = issueCtx.blockedAt;
|
|
84
97
|
issueCtx.blockedAt = null;
|
|
85
|
-
log(` resumed (counter reset)`);
|
|
98
|
+
log(` resumed from blocked (counter reset, since: ${issueCtx.countSince})`);
|
|
86
99
|
if (config.warpmetricsApiKey) {
|
|
87
100
|
try {
|
|
88
101
|
await warp.closeIssueRun(config.warpmetricsApiKey, {
|
|
@@ -92,12 +105,13 @@ export async function watch() {
|
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
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 });
|
|
96
109
|
|
|
97
110
|
// Record outcome on the issue run if revision failed terminally
|
|
98
111
|
if (!result.success && issueCtx && config.warpmetricsApiKey) {
|
|
99
112
|
const name = result.reason === 'max_retries' ? 'Max Retries' : 'Revision Failed';
|
|
100
113
|
issueCtx.blockedAt = new Date().toISOString();
|
|
114
|
+
issueCtx.countSince = null;
|
|
101
115
|
try {
|
|
102
116
|
await warp.closeIssueRun(config.warpmetricsApiKey, {
|
|
103
117
|
runId: issueCtx.runId,
|