whitesmith 0.0.2 → 0.0.4
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/README.md +286 -88
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +90 -2
- package/dist/cli.js.map +1 -1
- package/dist/comment.d.ts.map +1 -1
- package/dist/comment.js +18 -11
- package/dist/comment.js.map +1 -1
- package/dist/git.d.ts +5 -3
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +20 -29
- package/dist/git.js.map +1 -1
- package/dist/harnesses/pi.d.ts.map +1 -1
- package/dist/harnesses/pi.js +22 -6
- package/dist/harnesses/pi.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +31 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +214 -10
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompts.d.ts +52 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +197 -0
- package/dist/prompts.js.map +1 -1
- package/dist/providers/github-ci.d.ts +40 -0
- package/dist/providers/github-ci.d.ts.map +1 -1
- package/dist/providers/github-ci.js +463 -213
- package/dist/providers/github-ci.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/review.d.ts +48 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +221 -0
- package/dist/review.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +116 -3
- package/src/comment.ts +20 -14
- package/src/git.ts +23 -30
- package/src/harnesses/pi.ts +27 -6
- package/src/index.ts +9 -1
- package/src/orchestrator.ts +253 -14
- package/src/prompts.ts +239 -0
- package/src/providers/github-ci.ts +513 -217
- package/src/providers/index.ts +1 -1
- package/src/review.ts +290 -0
- package/src/types.ts +4 -0
package/src/providers/index.ts
CHANGED
package/src/review.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type {AgentHarness} from './harnesses/agent-harness.js';
|
|
4
|
+
import type {IssueProvider} from './providers/issue-provider.js';
|
|
5
|
+
import {GitManager} from './git.js';
|
|
6
|
+
import {TaskManager} from './task-manager.js';
|
|
7
|
+
import {
|
|
8
|
+
buildReviewTaskProposalPrompt,
|
|
9
|
+
buildReviewImplementationPRPrompt,
|
|
10
|
+
buildReviewTaskCompletionPrompt,
|
|
11
|
+
} from './prompts.js';
|
|
12
|
+
|
|
13
|
+
export type ReviewVerdict = 'approve' | 'request_changes' | 'unknown';
|
|
14
|
+
|
|
15
|
+
export interface ReviewResult {
|
|
16
|
+
/** The full review text (null if agent produced no output) */
|
|
17
|
+
response: string | null;
|
|
18
|
+
/** Parsed verdict from the review response */
|
|
19
|
+
verdict: ReviewVerdict;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ReviewConfig {
|
|
23
|
+
/** Working directory (the repo) */
|
|
24
|
+
workDir: string;
|
|
25
|
+
/** GitHub repo in "owner/repo" format (auto-detected if not set) */
|
|
26
|
+
repo?: string;
|
|
27
|
+
/** Log file path */
|
|
28
|
+
logFile?: string;
|
|
29
|
+
/** Whether to post the review as a GitHub comment */
|
|
30
|
+
post: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ReviewTarget =
|
|
34
|
+
| {type: 'pr'; number: number}
|
|
35
|
+
| {type: 'issue-tasks'; issueNumber: number}
|
|
36
|
+
| {type: 'issue-tasks-completed'; issueNumber: number};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Perform a review.
|
|
40
|
+
*
|
|
41
|
+
* - `pr`: Review a PR (examine the diff, check for bugs, quality, etc.)
|
|
42
|
+
* - `issue-tasks`: Review that proposed tasks are detailed and precise enough
|
|
43
|
+
* - `issue-tasks-completed`: Review that completed tasks were followed properly and check for bugs
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Parse the verdict from the review response text.
|
|
47
|
+
* Looks for a "VERDICT: APPROVE" or "VERDICT: REQUEST_CHANGES" line.
|
|
48
|
+
*/
|
|
49
|
+
export function parseReviewVerdict(response: string | null): ReviewVerdict {
|
|
50
|
+
if (!response) return 'unknown';
|
|
51
|
+
|
|
52
|
+
// Look for explicit verdict line (case-insensitive)
|
|
53
|
+
const verdictMatch = response.match(/^\s*\*{0,2}VERDICT\*{0,2}\s*[::]\s*(\S+)/im);
|
|
54
|
+
if (verdictMatch) {
|
|
55
|
+
const v = verdictMatch[1].toLowerCase().replace(/[^a-z_]/g, '');
|
|
56
|
+
if (v === 'approve' || v === 'approved') return 'approve';
|
|
57
|
+
if (v.includes('request') || v.includes('change') || v.includes('reject')) {
|
|
58
|
+
return 'request_changes';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback: look for common patterns
|
|
63
|
+
const lower = response.toLowerCase();
|
|
64
|
+
if (lower.includes('overall assessment: approve') || lower.includes('✅ approved')) {
|
|
65
|
+
return 'approve';
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
lower.includes('overall assessment: request changes') ||
|
|
69
|
+
lower.includes('❌ request changes')
|
|
70
|
+
) {
|
|
71
|
+
return 'request_changes';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function performReview(
|
|
78
|
+
target: ReviewTarget,
|
|
79
|
+
config: ReviewConfig,
|
|
80
|
+
issues: IssueProvider,
|
|
81
|
+
agent: AgentHarness,
|
|
82
|
+
): Promise<ReviewResult> {
|
|
83
|
+
const git = new GitManager(config.workDir);
|
|
84
|
+
const responseFile = '.whitesmith-review.md';
|
|
85
|
+
|
|
86
|
+
await git.fetch();
|
|
87
|
+
|
|
88
|
+
let prompt: string;
|
|
89
|
+
let postTarget: number; // issue/PR number to post the comment on
|
|
90
|
+
|
|
91
|
+
switch (target.type) {
|
|
92
|
+
case 'pr': {
|
|
93
|
+
const pr = await issues.getPR(target.number);
|
|
94
|
+
if (!pr) {
|
|
95
|
+
throw new Error(`Could not find PR #${target.number}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`Reviewing PR #${target.number}: ${pr.title}`);
|
|
99
|
+
console.log(`Branch: ${pr.branch}`);
|
|
100
|
+
|
|
101
|
+
// Checkout the PR branch so the agent can inspect the code
|
|
102
|
+
await git.checkout(pr.branch);
|
|
103
|
+
|
|
104
|
+
// Try to find the parent issue number from the branch name
|
|
105
|
+
const issueMatch = pr.branch.match(/^(?:issue|investigate)\/(\d+)$/);
|
|
106
|
+
let parentIssue = null;
|
|
107
|
+
if (issueMatch) {
|
|
108
|
+
try {
|
|
109
|
+
parentIssue = await issues.getIssue(parseInt(issueMatch[1], 10));
|
|
110
|
+
} catch {
|
|
111
|
+
// Issue might not exist
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
prompt = buildReviewImplementationPRPrompt({
|
|
116
|
+
prNumber: target.number,
|
|
117
|
+
prTitle: pr.title,
|
|
118
|
+
prBody: pr.body,
|
|
119
|
+
prBranch: pr.branch,
|
|
120
|
+
prUrl: pr.url,
|
|
121
|
+
parentIssue: parentIssue
|
|
122
|
+
? {
|
|
123
|
+
number: parentIssue.number,
|
|
124
|
+
title: parentIssue.title,
|
|
125
|
+
body: parentIssue.body,
|
|
126
|
+
url: parentIssue.url,
|
|
127
|
+
}
|
|
128
|
+
: undefined,
|
|
129
|
+
responseFile,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
postTarget = target.number;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case 'issue-tasks': {
|
|
137
|
+
const issue = await issues.getIssue(target.issueNumber);
|
|
138
|
+
console.log(`Reviewing task proposal for issue #${target.issueNumber}: ${issue.title}`);
|
|
139
|
+
|
|
140
|
+
// Find the task proposal PR
|
|
141
|
+
const taskPR = await issues.getPRForBranch(`investigate/${target.issueNumber}`);
|
|
142
|
+
if (taskPR) {
|
|
143
|
+
// Checkout the investigate branch to see the tasks
|
|
144
|
+
await git.checkout(`investigate/${target.issueNumber}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const taskManager = new TaskManager(config.workDir);
|
|
148
|
+
const tasks = taskManager.listTasks(target.issueNumber);
|
|
149
|
+
|
|
150
|
+
prompt = buildReviewTaskProposalPrompt({
|
|
151
|
+
issueNumber: target.issueNumber,
|
|
152
|
+
issueTitle: issue.title,
|
|
153
|
+
issueBody: issue.body,
|
|
154
|
+
issueUrl: issue.url,
|
|
155
|
+
tasks: tasks.map((t) => ({
|
|
156
|
+
id: t.id,
|
|
157
|
+
title: t.title,
|
|
158
|
+
content: t.content,
|
|
159
|
+
filePath: t.filePath,
|
|
160
|
+
})),
|
|
161
|
+
taskPRUrl: taskPR?.url,
|
|
162
|
+
responseFile,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
postTarget = taskPR?.number ?? target.issueNumber;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'issue-tasks-completed': {
|
|
170
|
+
const issue = await issues.getIssue(target.issueNumber);
|
|
171
|
+
console.log(`Reviewing completed tasks for issue #${target.issueNumber}: ${issue.title}`);
|
|
172
|
+
|
|
173
|
+
// Find the implementation PR
|
|
174
|
+
const implPR = await issues.getPRForBranch(`issue/${target.issueNumber}`);
|
|
175
|
+
if (implPR) {
|
|
176
|
+
// Checkout the issue branch
|
|
177
|
+
await git.checkout(`issue/${target.issueNumber}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
prompt = buildReviewTaskCompletionPrompt({
|
|
181
|
+
issueNumber: target.issueNumber,
|
|
182
|
+
issueTitle: issue.title,
|
|
183
|
+
issueBody: issue.body,
|
|
184
|
+
issueUrl: issue.url,
|
|
185
|
+
implPRUrl: implPR?.url,
|
|
186
|
+
implBranch: `issue/${target.issueNumber}`,
|
|
187
|
+
responseFile,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
postTarget = implPR?.number ?? target.issueNumber;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const {exitCode} = await agent.run({
|
|
196
|
+
prompt,
|
|
197
|
+
workDir: config.workDir,
|
|
198
|
+
logFile: config.logFile,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (exitCode !== 0) {
|
|
202
|
+
throw new Error(`Agent failed with exit code ${exitCode}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Read the response file
|
|
206
|
+
const responsePath = path.join(config.workDir, responseFile);
|
|
207
|
+
let response: string | null = null;
|
|
208
|
+
if (fs.existsSync(responsePath)) {
|
|
209
|
+
response = fs.readFileSync(responsePath, 'utf-8');
|
|
210
|
+
try {
|
|
211
|
+
fs.unlinkSync(responsePath);
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Discard any changes the agent made (review is read-only)
|
|
218
|
+
try {
|
|
219
|
+
const hasChanges = await git.hasChanges();
|
|
220
|
+
if (hasChanges) {
|
|
221
|
+
// Reset any modifications — reviews should not change code
|
|
222
|
+
const {exec: execAsync} = await import('node:child_process');
|
|
223
|
+
const {promisify} = await import('node:util');
|
|
224
|
+
const execP = promisify(execAsync);
|
|
225
|
+
await execP('git checkout -- .', {cwd: config.workDir});
|
|
226
|
+
await execP('git clean -fd', {cwd: config.workDir});
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Return to main
|
|
233
|
+
await git.checkoutMain();
|
|
234
|
+
|
|
235
|
+
const verdict = parseReviewVerdict(response);
|
|
236
|
+
|
|
237
|
+
if (response) {
|
|
238
|
+
if (config.post) {
|
|
239
|
+
await issues.comment(postTarget, response);
|
|
240
|
+
console.log(`Review posted as comment on #${postTarget} (verdict: ${verdict})`);
|
|
241
|
+
} else {
|
|
242
|
+
process.stdout.write(response);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.log('Agent did not produce a review response.');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {response, verdict};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Auto-detect what kind of review to perform based on a PR number.
|
|
253
|
+
* Inspects the branch name to determine if it's a task proposal or implementation.
|
|
254
|
+
*/
|
|
255
|
+
export async function detectReviewTarget(
|
|
256
|
+
number: number,
|
|
257
|
+
issues: IssueProvider,
|
|
258
|
+
): Promise<ReviewTarget> {
|
|
259
|
+
const pr = await issues.getPR(number);
|
|
260
|
+
if (pr) {
|
|
261
|
+
// It's a PR — check the branch name
|
|
262
|
+
const investigateMatch = pr.branch.match(/^investigate\/(\d+)$/);
|
|
263
|
+
if (investigateMatch) {
|
|
264
|
+
return {type: 'issue-tasks', issueNumber: parseInt(investigateMatch[1], 10)};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const issueMatch = pr.branch.match(/^issue\/(\d+)$/);
|
|
268
|
+
if (issueMatch) {
|
|
269
|
+
return {type: 'issue-tasks-completed', issueNumber: parseInt(issueMatch[1], 10)};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Generic PR review
|
|
273
|
+
return {type: 'pr', number};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// It's an issue — check if it has tasks
|
|
277
|
+
const issue = await issues.getIssue(number);
|
|
278
|
+
const hasTasksAccepted = issue.labels.some((l) => l.includes('tasks-accepted'));
|
|
279
|
+
const hasTasksProposed = issue.labels.some((l) => l.includes('tasks-proposed'));
|
|
280
|
+
|
|
281
|
+
if (hasTasksAccepted) {
|
|
282
|
+
return {type: 'issue-tasks-completed', issueNumber: number};
|
|
283
|
+
}
|
|
284
|
+
if (hasTasksProposed) {
|
|
285
|
+
return {type: 'issue-tasks', issueNumber: number};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Default: treat as issue tasks review
|
|
289
|
+
return {type: 'issue-tasks', issueNumber: number};
|
|
290
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -82,10 +82,14 @@ export interface DevPulseConfig {
|
|
|
82
82
|
dryRun: boolean;
|
|
83
83
|
/** Enable auto-work mode (auto-approve task PRs) */
|
|
84
84
|
autoWork: boolean;
|
|
85
|
+
/** Enable review step after PRs are created (on by default) */
|
|
86
|
+
review: boolean;
|
|
85
87
|
/** Log file path */
|
|
86
88
|
logFile?: string;
|
|
87
89
|
/** GitHub repo in "owner/repo" format (auto-detected if not set) */
|
|
88
90
|
repo?: string;
|
|
91
|
+
/** Target a single issue number (single-issue run mode) */
|
|
92
|
+
issueNumber?: number;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|