@warpmetrics/coder 0.2.5 → 0.2.7
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/agent.js +4 -3
- package/src/boards/github-projects.js +8 -0
- package/src/git.js +8 -0
- package/src/revise.js +34 -7
- package/src/warp.js +38 -5
- package/src/watch.js +88 -4
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -12,7 +12,7 @@ import { reflect } from './reflect.js';
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_DIR = '.warp-coder';
|
|
14
14
|
|
|
15
|
-
export async function implement(item, { board, config, log }) {
|
|
15
|
+
export async function implement(item, { board, config, log, refActId }) {
|
|
16
16
|
const issueNumber = item.content?.number;
|
|
17
17
|
const issueTitle = item.content?.title || `Issue #${issueNumber}`;
|
|
18
18
|
const issueBody = item.content?.body || '';
|
|
@@ -45,6 +45,7 @@ export async function implement(item, { board, config, log }) {
|
|
|
45
45
|
repo: repoName,
|
|
46
46
|
issueNumber,
|
|
47
47
|
issueTitle,
|
|
48
|
+
refActId,
|
|
48
49
|
});
|
|
49
50
|
runId = pipeline.runId;
|
|
50
51
|
groupId = pipeline.groupId;
|
|
@@ -203,9 +204,9 @@ export async function implement(item, { board, config, log }) {
|
|
|
203
204
|
log(` outcome: ${outcome.name}`);
|
|
204
205
|
|
|
205
206
|
// Emit act so warp-review can link its run as a follow-up
|
|
206
|
-
if (success && actId) {
|
|
207
|
+
if (success && actId && outcome.runOutcomeId) {
|
|
207
208
|
await warp.emitAct(config.warpmetricsApiKey, {
|
|
208
|
-
outcomeId: outcome.
|
|
209
|
+
outcomeId: outcome.runOutcomeId,
|
|
209
210
|
actId,
|
|
210
211
|
name: 'review',
|
|
211
212
|
});
|
|
@@ -111,6 +111,14 @@ export function create({ project, owner, statusField = 'Status', columns = {} })
|
|
|
111
111
|
const repo = item.content.repository || `${owner}/${item.content.repository}`;
|
|
112
112
|
const reviews = ghJson(`api repos/${repo}/pulls/${prNumber}/reviews`);
|
|
113
113
|
item._prNumber = prNumber;
|
|
114
|
+
|
|
115
|
+
// Parse act ID from the most recent warp-review review body
|
|
116
|
+
const actMatch = reviews
|
|
117
|
+
?.slice().reverse()
|
|
118
|
+
.map(r => (r.body || '').match(/<!-- wm:act:(wm_act_\w+) -->/))
|
|
119
|
+
.find(m => m);
|
|
120
|
+
if (actMatch) item._reviewActId = actMatch[1];
|
|
121
|
+
|
|
114
122
|
if (reviews?.some(r => r.state === 'APPROVED')) {
|
|
115
123
|
approved.push(item);
|
|
116
124
|
} else if (reviews?.some(r => r.state === 'COMMENTED' || r.state === 'CHANGES_REQUESTED')) {
|
package/src/git.js
CHANGED
|
@@ -59,6 +59,14 @@ export function getReviewComments(prNumber, { repo }) {
|
|
|
59
59
|
return JSON.parse(out);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export function updatePRBody(prNumber, { repo, body }) {
|
|
63
|
+
run(`gh pr edit ${prNumber} --repo ${repo} --body ${JSON.stringify(body)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getPRBody(prNumber, { repo }) {
|
|
67
|
+
return run(`gh pr view ${prNumber} --repo ${repo} --json body --jq .body`);
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
export function getPRBranch(prNumber, { repo }) {
|
|
63
71
|
const out = run(`gh pr view ${prNumber} --repo ${repo} --json headRefName --jq .headRefName`);
|
|
64
72
|
return out;
|
package/src/revise.js
CHANGED
|
@@ -12,7 +12,7 @@ import { reflect } from './reflect.js';
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_DIR = '.warp-coder';
|
|
14
14
|
|
|
15
|
-
export async function revise(item, { board, config, log }) {
|
|
15
|
+
export async function revise(item, { board, config, log, refActId }) {
|
|
16
16
|
const prNumber = item._prNumber || item.content?.number;
|
|
17
17
|
const repo = config.repo;
|
|
18
18
|
const repoName = repo.replace(/\.git$/, '').replace(/^.*github\.com[:\/]/, '');
|
|
@@ -20,6 +20,9 @@ export async function revise(item, { board, config, log }) {
|
|
|
20
20
|
const workdir = join(tmpdir(), 'warp-coder', `revise-${prNumber}`);
|
|
21
21
|
const configDir = join(process.cwd(), CONFIG_DIR);
|
|
22
22
|
|
|
23
|
+
// Pre-generate act ID for chaining (update PR body so warp-review can link next review)
|
|
24
|
+
const actId = config.warpmetricsApiKey ? warp.generateId('act') : null;
|
|
25
|
+
|
|
23
26
|
log(`Revising PR #${prNumber}`);
|
|
24
27
|
|
|
25
28
|
// Move to In Progress
|
|
@@ -33,13 +36,13 @@ export async function revise(item, { board, config, log }) {
|
|
|
33
36
|
// Check revision limit
|
|
34
37
|
if (config.warpmetricsApiKey) {
|
|
35
38
|
try {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
log(` revision limit reached (${
|
|
39
|
+
const revisionCount = await warp.countRevisions(config.warpmetricsApiKey, { prNumber, repo: repoName });
|
|
40
|
+
if (revisionCount >= maxRevisions) {
|
|
41
|
+
log(` revision limit reached (${revisionCount}/${maxRevisions}) — moving to Blocked`);
|
|
39
42
|
try { await board.moveToBlocked(item); } catch {}
|
|
40
|
-
return false;
|
|
43
|
+
return { success: false, reason: 'max_retries', count: revisionCount };
|
|
41
44
|
}
|
|
42
|
-
log(` revision ${
|
|
45
|
+
log(` revision ${revisionCount + 1}/${maxRevisions}`);
|
|
43
46
|
} catch (err) {
|
|
44
47
|
log(` warning: revision check failed: ${err.message}`);
|
|
45
48
|
}
|
|
@@ -54,6 +57,7 @@ export async function revise(item, { board, config, log }) {
|
|
|
54
57
|
step: 'revise',
|
|
55
58
|
repo: repoName,
|
|
56
59
|
prNumber,
|
|
60
|
+
refActId,
|
|
57
61
|
});
|
|
58
62
|
runId = pipeline.runId;
|
|
59
63
|
groupId = pipeline.groupId;
|
|
@@ -182,6 +186,20 @@ export async function revise(item, { board, config, log }) {
|
|
|
182
186
|
log(' pushing...');
|
|
183
187
|
git.push(workdir, branch);
|
|
184
188
|
|
|
189
|
+
// Update PR body with new act ID for next review cycle
|
|
190
|
+
if (actId) {
|
|
191
|
+
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} -->`;
|
|
196
|
+
}
|
|
197
|
+
git.updatePRBody(prNumber, { repo: repoName, body });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log(` warning: could not update PR body with act ID: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
// Move back to In Review
|
|
186
204
|
try {
|
|
187
205
|
await board.moveToReview(item);
|
|
@@ -212,6 +230,15 @@ export async function revise(item, { board, config, log }) {
|
|
|
212
230
|
reviewCommentCount: reviewComments.length,
|
|
213
231
|
});
|
|
214
232
|
log(` outcome: ${outcome.name}`);
|
|
233
|
+
|
|
234
|
+
// Emit act so warp-review can link its next review as a follow-up
|
|
235
|
+
if (success && actId && outcome.runOutcomeId) {
|
|
236
|
+
await warp.emitAct(config.warpmetricsApiKey, {
|
|
237
|
+
outcomeId: outcome.runOutcomeId,
|
|
238
|
+
actId,
|
|
239
|
+
name: 'review',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
215
242
|
} catch (err) {
|
|
216
243
|
log(` warning: outcome recording failed: ${err.message}`);
|
|
217
244
|
}
|
|
@@ -241,5 +268,5 @@ export async function revise(item, { board, config, log }) {
|
|
|
241
268
|
rmSync(workdir, { recursive: true, force: true });
|
|
242
269
|
}
|
|
243
270
|
|
|
244
|
-
return success;
|
|
271
|
+
return { success, reason: success ? 'ok' : 'error' };
|
|
245
272
|
}
|
package/src/warp.js
CHANGED
|
@@ -69,6 +69,8 @@ export async function registerClassifications(apiKey) {
|
|
|
69
69
|
const items = [
|
|
70
70
|
{ name: 'PR Created', classification: 'success' },
|
|
71
71
|
{ name: 'Fixes Applied', classification: 'success' },
|
|
72
|
+
{ name: 'Merged', classification: 'success' },
|
|
73
|
+
{ name: 'Shipped', classification: 'success' },
|
|
72
74
|
{ name: 'Issue Understood', classification: 'success' },
|
|
73
75
|
{ name: 'Needs Clarification', classification: 'neutral' },
|
|
74
76
|
{ name: 'Needs Human', classification: 'neutral' },
|
|
@@ -94,11 +96,37 @@ export async function registerClassifications(apiKey) {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Issue run — the root of the lifecycle chain
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export async function createIssueRun(apiKey, { repo, issueNumber, issueTitle }) {
|
|
104
|
+
const runId = generateId('run');
|
|
105
|
+
const outcomeId = generateId('oc');
|
|
106
|
+
const actId = generateId('act');
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
|
|
109
|
+
await sendEvents(apiKey, {
|
|
110
|
+
runs: [{ id: runId, label: 'issue', opts: { repo, issue: String(issueNumber), title: issueTitle }, refId: null, timestamp: now }],
|
|
111
|
+
outcomes: [{ id: outcomeId, refId: runId, name: 'Started', opts: null, timestamp: now }],
|
|
112
|
+
acts: [{ id: actId, refId: outcomeId, name: 'implement', opts: null, timestamp: now }],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { runId, actId };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function closeIssueRun(apiKey, { runId, name, opts }) {
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
await sendEvents(apiKey, {
|
|
121
|
+
outcomes: [{ id: generateId('oc'), refId: runId, name, opts: opts || null, timestamp: now }],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
97
125
|
// ---------------------------------------------------------------------------
|
|
98
126
|
// Pipeline helpers — start run/group and record outcome
|
|
99
127
|
// ---------------------------------------------------------------------------
|
|
100
128
|
|
|
101
|
-
export async function startPipeline(apiKey, { step, repo, issueNumber, issueTitle, prNumber }) {
|
|
129
|
+
export async function startPipeline(apiKey, { step, repo, issueNumber, issueTitle, prNumber, refActId }) {
|
|
102
130
|
const runId = generateId('run');
|
|
103
131
|
const groupId = generateId('grp');
|
|
104
132
|
const now = new Date().toISOString();
|
|
@@ -109,7 +137,7 @@ export async function startPipeline(apiKey, { step, repo, issueNumber, issueTitl
|
|
|
109
137
|
if (prNumber) opts.pr_number = String(prNumber);
|
|
110
138
|
|
|
111
139
|
await sendEvents(apiKey, {
|
|
112
|
-
runs: [{ id: runId, label: 'agent-pipeline', opts, refId: null, timestamp: now }],
|
|
140
|
+
runs: [{ id: runId, label: 'agent-pipeline', opts, refId: refActId || null, timestamp: now }],
|
|
113
141
|
groups: [{ id: groupId, label: step, opts: { triggered_at: now }, timestamp: now }],
|
|
114
142
|
links: [{ parentId: runId, childId: groupId, type: 'group', timestamp: now }],
|
|
115
143
|
});
|
|
@@ -121,6 +149,7 @@ export async function recordOutcome(apiKey, { runId, groupId }, { step, success,
|
|
|
121
149
|
const names = {
|
|
122
150
|
implement: { true: 'PR Created', false: 'Implementation Failed' },
|
|
123
151
|
revise: { true: 'Fixes Applied', false: 'Revision Failed' },
|
|
152
|
+
merge: { true: 'Merged', false: 'Merge Failed' },
|
|
124
153
|
};
|
|
125
154
|
|
|
126
155
|
const name = names[step]?.[String(success)] || `${step}: ${success ? 'success' : 'failure'}`;
|
|
@@ -134,16 +163,20 @@ export async function recordOutcome(apiKey, { runId, groupId }, { step, success,
|
|
|
134
163
|
if (prNumber) opts.pr_number = String(prNumber);
|
|
135
164
|
if (reviewCommentCount) opts.review_comments = String(reviewCommentCount);
|
|
136
165
|
|
|
166
|
+
const groupOutcomeId = generateId('oc');
|
|
137
167
|
const outcomes = [
|
|
138
|
-
{ id:
|
|
168
|
+
{ id: groupOutcomeId, refId: groupId, name, opts, timestamp: now },
|
|
139
169
|
];
|
|
170
|
+
|
|
171
|
+
let runOutcomeId = null;
|
|
140
172
|
if (runId) {
|
|
141
|
-
|
|
173
|
+
runOutcomeId = generateId('oc');
|
|
174
|
+
outcomes.push({ id: runOutcomeId, refId: runId, name, opts, timestamp: now });
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
await sendEvents(apiKey, { outcomes });
|
|
145
178
|
|
|
146
|
-
return { id:
|
|
179
|
+
return { id: groupOutcomeId, runOutcomeId, name };
|
|
147
180
|
}
|
|
148
181
|
|
|
149
182
|
export async function emitAct(apiKey, { outcomeId, actId, name, opts }) {
|
package/src/watch.js
CHANGED
|
@@ -5,12 +5,17 @@ import { createBoard } from './boards/index.js';
|
|
|
5
5
|
import { implement } from './agent.js';
|
|
6
6
|
import { revise } from './revise.js';
|
|
7
7
|
import * as git from './git.js';
|
|
8
|
+
import * as warp from './warp.js';
|
|
8
9
|
import { runHook } from './hooks.js';
|
|
9
10
|
|
|
10
11
|
export async function watch() {
|
|
11
12
|
const config = loadConfig();
|
|
12
13
|
const board = createBoard(config);
|
|
13
14
|
const pollInterval = (config.pollInterval || 30) * 1000;
|
|
15
|
+
const repoName = config.repo.replace(/\.git$/, '').replace(/^.*github\.com[:\/]/, '');
|
|
16
|
+
|
|
17
|
+
// Track issue runs across poll cycles (issueNumber → { runId })
|
|
18
|
+
const issueRuns = new Map();
|
|
14
19
|
|
|
15
20
|
let running = true;
|
|
16
21
|
let sleepResolve = null;
|
|
@@ -42,8 +47,26 @@ export async function watch() {
|
|
|
42
47
|
const todoItems = await board.listTodo();
|
|
43
48
|
if (todoItems.length > 0) {
|
|
44
49
|
const item = todoItems[0];
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const issueNumber = item.content?.number;
|
|
51
|
+
const issueTitle = item.content?.title;
|
|
52
|
+
log(`Found todo: #${issueNumber} — ${issueTitle}`);
|
|
53
|
+
|
|
54
|
+
// Create issue run (root of the lifecycle chain)
|
|
55
|
+
let implementActId = null;
|
|
56
|
+
if (config.warpmetricsApiKey && !issueRuns.has(issueNumber)) {
|
|
57
|
+
try {
|
|
58
|
+
const issue = await warp.createIssueRun(config.warpmetricsApiKey, {
|
|
59
|
+
repo: repoName, issueNumber, issueTitle,
|
|
60
|
+
});
|
|
61
|
+
issueRuns.set(issueNumber, { runId: issue.runId });
|
|
62
|
+
implementActId = issue.actId;
|
|
63
|
+
log(` issue run: ${issue.runId}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
log(` warning: issue run creation failed: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await implement(item, { board, config, log, refActId: implementActId });
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
// 2. Check for items needing revision
|
|
@@ -51,7 +74,24 @@ export async function watch() {
|
|
|
51
74
|
for (const item of reviewItems) {
|
|
52
75
|
if (!running) break;
|
|
53
76
|
log(`Found review feedback: PR #${item._prNumber || item.content?.number}`);
|
|
54
|
-
await revise(item, { board, config, log });
|
|
77
|
+
const result = await revise(item, { board, config, log, refActId: item._reviewActId });
|
|
78
|
+
|
|
79
|
+
// Record outcome on the issue run if revision failed terminally
|
|
80
|
+
const issueNumber = item.content?.number;
|
|
81
|
+
const issueCtx = issueNumber ? issueRuns.get(issueNumber) : null;
|
|
82
|
+
if (!result.success && issueCtx && config.warpmetricsApiKey) {
|
|
83
|
+
const name = result.reason === 'max_retries' ? 'Max Retries' : 'Revision Failed';
|
|
84
|
+
try {
|
|
85
|
+
await warp.closeIssueRun(config.warpmetricsApiKey, {
|
|
86
|
+
runId: issueCtx.runId,
|
|
87
|
+
name,
|
|
88
|
+
opts: { pr_number: String(item._prNumber || ''), revisions: String(result.count || '') },
|
|
89
|
+
});
|
|
90
|
+
log(` issue run: ${name}`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log(` warning: issue run outcome failed: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
55
95
|
}
|
|
56
96
|
|
|
57
97
|
// 3. Merge approved PRs
|
|
@@ -59,16 +99,60 @@ export async function watch() {
|
|
|
59
99
|
for (const item of approvedItems) {
|
|
60
100
|
if (!running) break;
|
|
61
101
|
const prNumber = item._prNumber || item.content?.number;
|
|
62
|
-
const
|
|
102
|
+
const refActId = item._reviewActId;
|
|
63
103
|
log(`Merging approved PR #${prNumber}`);
|
|
104
|
+
|
|
105
|
+
// Create merge run (follow-up of review's "merge" act)
|
|
106
|
+
let mergeRunId = null;
|
|
107
|
+
let mergeGroupId = null;
|
|
108
|
+
if (config.warpmetricsApiKey) {
|
|
109
|
+
try {
|
|
110
|
+
const pipeline = await warp.startPipeline(config.warpmetricsApiKey, {
|
|
111
|
+
step: 'merge', repo: repoName, prNumber, refActId,
|
|
112
|
+
});
|
|
113
|
+
mergeRunId = pipeline.runId;
|
|
114
|
+
mergeGroupId = pipeline.groupId;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log(` warning: merge pipeline failed: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
64
120
|
try {
|
|
65
121
|
runHook('onBeforeMerge', config, { prNumber, repo: repoName });
|
|
66
122
|
git.mergePR(prNumber, { repo: repoName });
|
|
67
123
|
runHook('onMerged', config, { prNumber, repo: repoName });
|
|
68
124
|
await board.moveToDone(item);
|
|
69
125
|
log(` merged and moved to Done`);
|
|
126
|
+
|
|
127
|
+
// Record merge outcome
|
|
128
|
+
if (config.warpmetricsApiKey && mergeGroupId) {
|
|
129
|
+
try {
|
|
130
|
+
await warp.recordOutcome(config.warpmetricsApiKey, { runId: mergeRunId, groupId: mergeGroupId }, {
|
|
131
|
+
step: 'merge', success: true, prNumber,
|
|
132
|
+
});
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Close the issue run with "Shipped"
|
|
137
|
+
const issueNumber = item.content?.number;
|
|
138
|
+
const issueCtx = issueRuns.get(issueNumber);
|
|
139
|
+
if (config.warpmetricsApiKey && issueCtx) {
|
|
140
|
+
try {
|
|
141
|
+
await warp.closeIssueRun(config.warpmetricsApiKey, {
|
|
142
|
+
runId: issueCtx.runId, name: 'Shipped',
|
|
143
|
+
});
|
|
144
|
+
issueRuns.delete(issueNumber);
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
70
147
|
} catch (err) {
|
|
71
148
|
log(` merge failed: ${err.message}`);
|
|
149
|
+
if (config.warpmetricsApiKey && mergeGroupId) {
|
|
150
|
+
try {
|
|
151
|
+
await warp.recordOutcome(config.warpmetricsApiKey, { runId: mergeRunId, groupId: mergeGroupId }, {
|
|
152
|
+
step: 'merge', success: false, error: err.message, prNumber,
|
|
153
|
+
});
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
72
156
|
}
|
|
73
157
|
}
|
|
74
158
|
} catch (err) {
|