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.
Files changed (50) hide show
  1. package/README.md +286 -88
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +90 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/comment.d.ts.map +1 -1
  6. package/dist/comment.js +18 -11
  7. package/dist/comment.js.map +1 -1
  8. package/dist/git.d.ts +5 -3
  9. package/dist/git.d.ts.map +1 -1
  10. package/dist/git.js +20 -29
  11. package/dist/git.js.map +1 -1
  12. package/dist/harnesses/pi.d.ts.map +1 -1
  13. package/dist/harnesses/pi.js +22 -6
  14. package/dist/harnesses/pi.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/orchestrator.d.ts +31 -3
  20. package/dist/orchestrator.d.ts.map +1 -1
  21. package/dist/orchestrator.js +214 -10
  22. package/dist/orchestrator.js.map +1 -1
  23. package/dist/prompts.d.ts +52 -0
  24. package/dist/prompts.d.ts.map +1 -1
  25. package/dist/prompts.js +197 -0
  26. package/dist/prompts.js.map +1 -1
  27. package/dist/providers/github-ci.d.ts +40 -0
  28. package/dist/providers/github-ci.d.ts.map +1 -1
  29. package/dist/providers/github-ci.js +463 -213
  30. package/dist/providers/github-ci.js.map +1 -1
  31. package/dist/providers/index.d.ts +1 -1
  32. package/dist/providers/index.d.ts.map +1 -1
  33. package/dist/review.d.ts +48 -0
  34. package/dist/review.d.ts.map +1 -0
  35. package/dist/review.js +221 -0
  36. package/dist/review.js.map +1 -0
  37. package/dist/types.d.ts +4 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli.ts +116 -3
  41. package/src/comment.ts +20 -14
  42. package/src/git.ts +23 -30
  43. package/src/harnesses/pi.ts +27 -6
  44. package/src/index.ts +9 -1
  45. package/src/orchestrator.ts +253 -14
  46. package/src/prompts.ts +239 -0
  47. package/src/providers/github-ci.ts +513 -217
  48. package/src/providers/index.ts +1 -1
  49. package/src/review.ts +290 -0
  50. package/src/types.ts +4 -0
@@ -1,3 +1,3 @@
1
1
  export type {IssueProvider} from './issue-provider.js';
2
2
  export {GitHubProvider} from './github.js';
3
- export type {AuthMode, InstallCIOptions} from './github-ci.js';
3
+ export type {AuthMode, InstallCIOptions, CIConfigFile, ProviderEntry} from './github-ci.js';
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
  /**