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/git.ts
CHANGED
|
@@ -13,6 +13,7 @@ export class GitManager {
|
|
|
13
13
|
|
|
14
14
|
constructor(workDir: string) {
|
|
15
15
|
this.workDir = workDir;
|
|
16
|
+
this.ensureExcluded();
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
private async git(args: string): Promise<string> {
|
|
@@ -28,17 +29,28 @@ export class GitManager {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Ensure .whitesmith-* is excluded from git via .git/info/exclude.
|
|
33
|
+
* This prevents the agent from accidentally committing temp files
|
|
34
|
+
* without requiring changes to the user's .gitignore.
|
|
32
35
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
private ensureExcluded(): void {
|
|
37
|
+
const gitDir = path.join(this.workDir, '.git');
|
|
38
|
+
if (!fs.existsSync(gitDir)) return;
|
|
39
|
+
|
|
40
|
+
const excludeDir = path.join(gitDir, 'info');
|
|
41
|
+
const excludeFile = path.join(excludeDir, 'exclude');
|
|
42
|
+
const pattern = '.whitesmith-*';
|
|
43
|
+
|
|
44
|
+
fs.mkdirSync(excludeDir, {recursive: true});
|
|
45
|
+
|
|
46
|
+
let content = '';
|
|
47
|
+
if (fs.existsSync(excludeFile)) {
|
|
48
|
+
content = fs.readFileSync(excludeFile, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!content.split('\n').some((line) => line.trim() === pattern)) {
|
|
52
|
+
const separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
53
|
+
fs.appendFileSync(excludeFile, `${separator}${pattern}\n`);
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
@@ -72,31 +84,12 @@ export class GitManager {
|
|
|
72
84
|
/**
|
|
73
85
|
* Stage all changes and commit
|
|
74
86
|
*/
|
|
75
|
-
async commitAll(message: string
|
|
76
|
-
// Always exclude whitesmith temp files
|
|
77
|
-
const allExclude = ['.whitesmith-*', ...(exclude || [])];
|
|
78
|
-
|
|
79
|
-
// Remove any whitesmith temp files from the working tree
|
|
80
|
-
this.cleanupTempFiles();
|
|
81
|
-
|
|
87
|
+
async commitAll(message: string): Promise<boolean> {
|
|
82
88
|
// Check if there are changes
|
|
83
89
|
const status = await this.git('status --porcelain');
|
|
84
90
|
if (!status) return false;
|
|
85
91
|
|
|
86
|
-
// Add all then unstage excluded patterns
|
|
87
92
|
await this.git('add -A');
|
|
88
|
-
for (const pattern of allExclude) {
|
|
89
|
-
try {
|
|
90
|
-
await this.git(`reset HEAD -- ${pattern}`);
|
|
91
|
-
} catch {
|
|
92
|
-
// File might not be staged
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check if anything is still staged after exclusions
|
|
97
|
-
const staged = await this.git('diff --cached --name-only');
|
|
98
|
-
if (!staged) return false;
|
|
99
|
-
|
|
100
93
|
await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`);
|
|
101
94
|
return true;
|
|
102
95
|
}
|
package/src/harnesses/pi.ts
CHANGED
|
@@ -49,10 +49,29 @@ export class PiHarness implements AgentHarness {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Check auth.json
|
|
52
|
+
// Check for auth configuration (auth.json or models.json)
|
|
53
53
|
const homeDir = process.env.HOME || homedir();
|
|
54
54
|
const authJsonPath = path.join(homeDir, '.pi', 'agent', 'auth.json');
|
|
55
|
-
|
|
55
|
+
const modelsJsonPath = path.join(homeDir, '.pi', 'agent', 'models.json');
|
|
56
|
+
const hasAuthJson = fs.existsSync(authJsonPath);
|
|
57
|
+
const hasModelsJson = fs.existsSync(modelsJsonPath);
|
|
58
|
+
|
|
59
|
+
if (hasModelsJson) {
|
|
60
|
+
try {
|
|
61
|
+
const modelsData = JSON.parse(fs.readFileSync(modelsJsonPath, 'utf-8'));
|
|
62
|
+
const providers = Object.keys(modelsData.providers || {});
|
|
63
|
+
console.log(
|
|
64
|
+
`Models config found at ${modelsJsonPath} with providers: ${providers.join(', ')}`,
|
|
65
|
+
);
|
|
66
|
+
if (!modelsData.providers?.[this.provider]) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`WARNING: Provider '${this.provider}' not found in models.json (has: ${providers.join(', ')})`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} catch (e: any) {
|
|
72
|
+
console.warn(`WARNING: Could not parse models.json: ${e.message}`);
|
|
73
|
+
}
|
|
74
|
+
} else if (hasAuthJson) {
|
|
56
75
|
try {
|
|
57
76
|
const authData = JSON.parse(fs.readFileSync(authJsonPath, 'utf-8'));
|
|
58
77
|
const providers = Object.keys(authData);
|
|
@@ -66,7 +85,9 @@ export class PiHarness implements AgentHarness {
|
|
|
66
85
|
console.warn(`WARNING: Could not parse auth.json: ${e.message}`);
|
|
67
86
|
}
|
|
68
87
|
} else {
|
|
69
|
-
console.warn(
|
|
88
|
+
console.warn(
|
|
89
|
+
`WARNING: No auth configuration found (checked ${modelsJsonPath} and ${authJsonPath})`,
|
|
90
|
+
);
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
// Validate auth by making a minimal API call
|
|
@@ -87,9 +108,9 @@ export class PiHarness implements AgentHarness {
|
|
|
87
108
|
[stderr, stdout].filter(Boolean).join('\n') || error.message || 'unknown error';
|
|
88
109
|
throw new Error(
|
|
89
110
|
`Agent auth validation failed. Ensure valid credentials are configured.\n` +
|
|
90
|
-
`
|
|
91
|
-
`
|
|
92
|
-
`
|
|
111
|
+
`Configure providers via ~/.pi/agent/models.json or ~/.pi/agent/auth.json\n` +
|
|
112
|
+
`models.json exists: ${hasModelsJson}\n` +
|
|
113
|
+
`auth.json exists: ${hasAuthJson}\n` +
|
|
93
114
|
`HOME: ${homeDir}\n` +
|
|
94
115
|
`Details: ${details.slice(0, 800)}`,
|
|
95
116
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
export {Orchestrator} from './orchestrator.js';
|
|
2
2
|
export {TaskManager} from './task-manager.js';
|
|
3
3
|
export {GitManager} from './git.js';
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
buildInvestigatePrompt,
|
|
6
|
+
buildImplementPrompt,
|
|
7
|
+
buildReviewTaskProposalPrompt,
|
|
8
|
+
buildReviewImplementationPRPrompt,
|
|
9
|
+
buildReviewTaskCompletionPrompt,
|
|
10
|
+
} from './prompts.js';
|
|
11
|
+
export {performReview, detectReviewTarget, parseReviewVerdict} from './review.js';
|
|
12
|
+
export type {ReviewConfig, ReviewTarget, ReviewResult, ReviewVerdict} from './review.js';
|
|
5
13
|
|
|
6
14
|
export type {IssueProvider} from './providers/issue-provider.js';
|
|
7
15
|
export {GitHubProvider} from './providers/github.js';
|
package/src/orchestrator.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {TaskManager} from './task-manager.js';
|
|
|
6
6
|
import {GitManager} from './git.js';
|
|
7
7
|
import {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
|
|
8
8
|
import {isAutoWorkEnabled} from './auto-work.js';
|
|
9
|
+
import {performReview} from './review.js';
|
|
10
|
+
import type {ReviewResult} from './review.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Main orchestrator for whitesmith.
|
|
@@ -44,6 +46,9 @@ export class Orchestrator {
|
|
|
44
46
|
console.log(`Agent command: ${this.config.agentCmd}`);
|
|
45
47
|
console.log(`Provider: ${this.config.provider}`);
|
|
46
48
|
console.log(`Model: ${this.config.model}`);
|
|
49
|
+
if (this.config.issueNumber !== undefined) {
|
|
50
|
+
console.log(`Target issue: #${this.config.issueNumber}`);
|
|
51
|
+
}
|
|
47
52
|
console.log('');
|
|
48
53
|
|
|
49
54
|
// Skip agent validation and label creation in dry-run mode
|
|
@@ -57,6 +62,12 @@ export class Orchestrator {
|
|
|
57
62
|
await this.issues.ensureLabels(Object.values(LABELS));
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
// Delegate to single-issue mode if --issue is set
|
|
66
|
+
if (this.config.issueNumber !== undefined) {
|
|
67
|
+
await this.runForIssue(this.config.issueNumber);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
for (let i = 1; i <= this.config.maxIterations; i++) {
|
|
61
72
|
console.log('');
|
|
62
73
|
console.log(`=== Iteration ${i}/${this.config.maxIterations} ===`);
|
|
@@ -122,6 +133,146 @@ export class Orchestrator {
|
|
|
122
133
|
console.log('=== Iteration limit reached ===');
|
|
123
134
|
}
|
|
124
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Run the full pipeline for a single issue.
|
|
138
|
+
*
|
|
139
|
+
* Re-fetches the issue after each action to get updated labels, then decides
|
|
140
|
+
* the next action based on the current state. Loops until idle or the
|
|
141
|
+
* iteration limit is reached.
|
|
142
|
+
*/
|
|
143
|
+
private async runForIssue(issueNumber: number): Promise<void> {
|
|
144
|
+
console.log(`Running single-issue mode for issue #${issueNumber}`);
|
|
145
|
+
|
|
146
|
+
for (let i = 1; i <= this.config.maxIterations; i++) {
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(`=== Iteration ${i}/${this.config.maxIterations} ===`);
|
|
149
|
+
|
|
150
|
+
// Make sure we're on main with latest
|
|
151
|
+
await this.git.fetch();
|
|
152
|
+
await this.git.checkoutMain();
|
|
153
|
+
|
|
154
|
+
// Re-fetch the issue to get current labels
|
|
155
|
+
const issue = await this.issues.getIssue(issueNumber);
|
|
156
|
+
|
|
157
|
+
// Determine the action based on the issue's current state
|
|
158
|
+
const action = await this.decideActionForIssue(issue);
|
|
159
|
+
console.log(`Action: ${action.type}`);
|
|
160
|
+
|
|
161
|
+
if (this.config.dryRun) {
|
|
162
|
+
switch (action.type) {
|
|
163
|
+
case 'reconcile':
|
|
164
|
+
console.log(`Would reconcile issue #${action.issue.number}: ${action.issue.title}`);
|
|
165
|
+
break;
|
|
166
|
+
case 'auto-approve':
|
|
167
|
+
console.log(
|
|
168
|
+
`Would auto-approve task PR for issue #${action.issue.number}: ${action.issue.title}`,
|
|
169
|
+
);
|
|
170
|
+
break;
|
|
171
|
+
case 'investigate':
|
|
172
|
+
console.log(`Would investigate issue #${action.issue.number}: ${action.issue.title}`);
|
|
173
|
+
break;
|
|
174
|
+
case 'implement':
|
|
175
|
+
console.log(
|
|
176
|
+
`Would implement task ${action.task.id}: ${action.task.title} (issue #${action.issue.number})`,
|
|
177
|
+
);
|
|
178
|
+
break;
|
|
179
|
+
case 'idle':
|
|
180
|
+
console.log('Nothing to do. Issue is either completed or no actions are applicable.');
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
switch (action.type) {
|
|
187
|
+
case 'reconcile':
|
|
188
|
+
await this.reconcile(action.issue);
|
|
189
|
+
break;
|
|
190
|
+
case 'auto-approve':
|
|
191
|
+
await this.autoApprove(action.issue);
|
|
192
|
+
break;
|
|
193
|
+
case 'investigate':
|
|
194
|
+
await this.investigate(action.issue);
|
|
195
|
+
break;
|
|
196
|
+
case 'implement':
|
|
197
|
+
await this.implement(action.task, action.issue);
|
|
198
|
+
break;
|
|
199
|
+
case 'idle':
|
|
200
|
+
console.log('Nothing to do. Issue is either completed or no actions are applicable.');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!this.config.noSleep && i < this.config.maxIterations) {
|
|
205
|
+
console.log('Sleeping 5s...');
|
|
206
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log('=== Iteration limit reached ===');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Decide the next action for a single issue based on its current labels.
|
|
216
|
+
*/
|
|
217
|
+
private async decideActionForIssue(issue: Issue): Promise<Action> {
|
|
218
|
+
const labels = issue.labels;
|
|
219
|
+
|
|
220
|
+
// Handle stale investigating label (crashed previous run)
|
|
221
|
+
if (labels.includes(LABELS.INVESTIGATING)) {
|
|
222
|
+
console.log(`Issue #${issue.number} has stale '${LABELS.INVESTIGATING}' label, clearing it`);
|
|
223
|
+
await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
|
|
224
|
+
// Treat as uninvestigated — re-investigate
|
|
225
|
+
return {type: 'investigate', issue};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// tasks-accepted: check if all tasks are done (reconcile) or implement next task
|
|
229
|
+
if (labels.includes(LABELS.TASKS_ACCEPTED)) {
|
|
230
|
+
const allDone = await this.allTasksCompletedOnBranch(issue.number);
|
|
231
|
+
if (allDone) {
|
|
232
|
+
return {type: 'reconcile', issue};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Find an available task to implement
|
|
236
|
+
const implementAction = await this.findAvailableTask([issue]);
|
|
237
|
+
if (implementAction) {
|
|
238
|
+
return implementAction;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {type: 'idle'};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// tasks-proposed: check if PR was already merged (tasks on main) → transition inline
|
|
245
|
+
if (labels.includes(LABELS.TASKS_PROPOSED)) {
|
|
246
|
+
if (this.tasks.hasRemainingTasks(issue.number)) {
|
|
247
|
+
// Tasks exist on main = investigate PR was merged
|
|
248
|
+
console.log(`Issue #${issue.number}: tasks PR merged, transitioning to tasks-accepted`);
|
|
249
|
+
await this.issues.removeLabel(issue.number, LABELS.TASKS_PROPOSED);
|
|
250
|
+
await this.issues.addLabel(issue.number, LABELS.TASKS_ACCEPTED);
|
|
251
|
+
// Find an available task to implement
|
|
252
|
+
const implementAction = await this.findAvailableTask([issue]);
|
|
253
|
+
if (implementAction) {
|
|
254
|
+
return implementAction;
|
|
255
|
+
}
|
|
256
|
+
return {type: 'idle'};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// PR not yet merged — auto-approve if auto-work is enabled
|
|
260
|
+
if (isAutoWorkEnabled(this.config, issue)) {
|
|
261
|
+
return {type: 'auto-approve', issue};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {type: 'idle'};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// completed: nothing to do
|
|
268
|
+
if (labels.includes(LABELS.COMPLETED)) {
|
|
269
|
+
return {type: 'idle'};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// No whitesmith labels — investigate
|
|
273
|
+
return {type: 'investigate', issue};
|
|
274
|
+
}
|
|
275
|
+
|
|
125
276
|
/**
|
|
126
277
|
* Check whether all tasks for an issue have been completed on the issue branch.
|
|
127
278
|
* Works without checking out the branch by inspecting the remote via git ls-tree.
|
|
@@ -141,11 +292,19 @@ export class Orchestrator {
|
|
|
141
292
|
*/
|
|
142
293
|
private async decideAction(): Promise<Action> {
|
|
143
294
|
// Priority 1: Reconcile — issues with tasks-accepted where all tasks are done
|
|
295
|
+
// but no PR exists yet (safety net for crash recovery).
|
|
144
296
|
const acceptedIssues = await this.issues.listIssues({labels: [LABELS.TASKS_ACCEPTED]});
|
|
145
297
|
for (const issue of acceptedIssues) {
|
|
146
298
|
const allDone = await this.allTasksCompletedOnBranch(issue.number);
|
|
147
299
|
if (allDone) {
|
|
148
|
-
|
|
300
|
+
// Only reconcile if no PR exists yet — if a PR is already open,
|
|
301
|
+
// the issue is waiting for merge and there's nothing more to do.
|
|
302
|
+
const branch = `issue/${issue.number}`;
|
|
303
|
+
const existingPR = await this.issues.getPRForBranch(branch);
|
|
304
|
+
if (!existingPR || existingPR.state === 'closed') {
|
|
305
|
+
return {type: 'reconcile', issue};
|
|
306
|
+
}
|
|
307
|
+
// PR exists and is open or merged — skip
|
|
149
308
|
}
|
|
150
309
|
}
|
|
151
310
|
|
|
@@ -226,13 +385,16 @@ export class Orchestrator {
|
|
|
226
385
|
}
|
|
227
386
|
|
|
228
387
|
/**
|
|
229
|
-
* Phase 1: Reconcile —
|
|
230
|
-
*
|
|
388
|
+
* Phase 1: Reconcile — safety net for crash recovery.
|
|
389
|
+
* Creates PR if all tasks are done on the issue branch but no PR exists
|
|
231
390
|
* (e.g. agent crashed after last task push but before PR creation).
|
|
391
|
+
*
|
|
392
|
+
* Does NOT close the issue — that happens when the PR is merged and the
|
|
393
|
+
* CLI `reconcile` command detects that task files are gone from main.
|
|
232
394
|
*/
|
|
233
395
|
private async reconcile(issue: Issue): Promise<void> {
|
|
234
396
|
console.log(`Reconciling issue #${issue.number}: ${issue.title}`);
|
|
235
|
-
console.log('All tasks completed.
|
|
397
|
+
console.log('All tasks completed on branch. Ensuring PR exists.');
|
|
236
398
|
|
|
237
399
|
// Safety net: ensure a PR exists for the issue branch
|
|
238
400
|
const branch = `issue/${issue.number}`;
|
|
@@ -253,22 +415,17 @@ export class Orchestrator {
|
|
|
253
415
|
body: `## Implementation for #${issue.number}\n\n${taskSummary}\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number}`,
|
|
254
416
|
});
|
|
255
417
|
console.log(`Safety net PR created: ${prUrl}`);
|
|
418
|
+
} else {
|
|
419
|
+
console.log(`PR already exists: ${existingPR.url}`);
|
|
256
420
|
}
|
|
257
421
|
}
|
|
258
422
|
|
|
259
|
-
|
|
260
|
-
await this.issues.removeLabel(issue.number, LABELS.TASKS_ACCEPTED);
|
|
261
|
-
await this.issues.comment(
|
|
262
|
-
issue.number,
|
|
263
|
-
`✅ All tasks for this issue have been implemented and merged. Closing.`,
|
|
264
|
-
);
|
|
265
|
-
await this.issues.closeIssue(issue.number);
|
|
266
|
-
|
|
267
|
-
console.log(`Issue #${issue.number} closed.`);
|
|
423
|
+
console.log(`Issue #${issue.number} awaiting PR merge.`);
|
|
268
424
|
}
|
|
269
425
|
|
|
270
426
|
/**
|
|
271
|
-
* Phase 1.5: Auto-approve — merge the task-proposal PR when auto-work is enabled
|
|
427
|
+
* Phase 1.5: Auto-approve — merge the task-proposal PR when auto-work is enabled.
|
|
428
|
+
* When review is enabled, runs a review first and only merges if approved.
|
|
272
429
|
*/
|
|
273
430
|
private async autoApprove(issue: Issue): Promise<void> {
|
|
274
431
|
console.log(`Auto-approving task PR for issue #${issue.number}: ${issue.title}`);
|
|
@@ -281,6 +438,27 @@ export class Orchestrator {
|
|
|
281
438
|
return;
|
|
282
439
|
}
|
|
283
440
|
|
|
441
|
+
// Run review before merging (if review is enabled)
|
|
442
|
+
if (this.config.review) {
|
|
443
|
+
let reviewResult: ReviewResult | null = null;
|
|
444
|
+
try {
|
|
445
|
+
reviewResult = await this.reviewTaskProposal(issue.number);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error('Review failed:', error instanceof Error ? error.message : error);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (reviewResult && reviewResult.verdict === 'request_changes') {
|
|
451
|
+
console.log(`Review requested changes for task PR #${pr.number}. Skipping auto-merge.`);
|
|
452
|
+
await this.issues.comment(
|
|
453
|
+
issue.number,
|
|
454
|
+
`🔍 Review of task PR #${pr.number} requested changes. Auto-merge skipped — please review manually.`,
|
|
455
|
+
);
|
|
456
|
+
// Remove tasks-proposed so auto-approve doesn't retry every iteration
|
|
457
|
+
await this.issues.removeLabel(issue.number, LABELS.TASKS_PROPOSED);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
284
462
|
await this.issues.mergePR(pr.number);
|
|
285
463
|
console.log(`Merged PR #${pr.number}: ${pr.url}`);
|
|
286
464
|
|
|
@@ -396,6 +574,17 @@ export class Orchestrator {
|
|
|
396
574
|
);
|
|
397
575
|
|
|
398
576
|
console.log(`PR created: ${prUrl}`);
|
|
577
|
+
|
|
578
|
+
// Queue review of the task proposal (skip if auto-work — auto-approve will review)
|
|
579
|
+
if (this.config.review && !isAutoWorkEnabled(this.config, issue)) {
|
|
580
|
+
await this.git.checkoutMain();
|
|
581
|
+
try {
|
|
582
|
+
await this.reviewTaskProposal(issue.number);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error('Review failed:', error instanceof Error ? error.message : error);
|
|
585
|
+
}
|
|
586
|
+
return; // Already on main after review
|
|
587
|
+
}
|
|
399
588
|
}
|
|
400
589
|
} catch (error) {
|
|
401
590
|
console.error('Investigation failed:', error instanceof Error ? error.message : error);
|
|
@@ -496,6 +685,16 @@ export class Orchestrator {
|
|
|
496
685
|
}
|
|
497
686
|
|
|
498
687
|
console.log(`PR created: ${prUrl}`);
|
|
688
|
+
// Queue review of the implementation PR
|
|
689
|
+
if (this.config.review) {
|
|
690
|
+
await this.git.checkoutMain();
|
|
691
|
+
try {
|
|
692
|
+
await this.reviewImplementationPR(issue.number);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('Review failed:', error instanceof Error ? error.message : error);
|
|
695
|
+
}
|
|
696
|
+
return; // Already on main after review
|
|
697
|
+
}
|
|
499
698
|
} else {
|
|
500
699
|
console.log(
|
|
501
700
|
`Task ${task.id} committed. ${remainingTasks.length} task(s) remaining for issue #${issue.number}.`,
|
|
@@ -509,4 +708,44 @@ export class Orchestrator {
|
|
|
509
708
|
// Return to main
|
|
510
709
|
await this.git.checkoutMain();
|
|
511
710
|
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Review a task proposal (investigate PR).
|
|
714
|
+
* Posts the review as a comment on the task proposal PR.
|
|
715
|
+
* Returns the review result so callers can check the verdict.
|
|
716
|
+
*/
|
|
717
|
+
private async reviewTaskProposal(issueNumber: number): Promise<ReviewResult> {
|
|
718
|
+
console.log(`Reviewing task proposal for issue #${issueNumber}...`);
|
|
719
|
+
return performReview(
|
|
720
|
+
{type: 'issue-tasks', issueNumber},
|
|
721
|
+
{
|
|
722
|
+
workDir: this.config.workDir,
|
|
723
|
+
repo: this.config.repo,
|
|
724
|
+
logFile: this.config.logFile,
|
|
725
|
+
post: !this.config.noPush,
|
|
726
|
+
},
|
|
727
|
+
this.issues,
|
|
728
|
+
this.agent,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Review an implementation PR (all tasks completed for an issue).
|
|
734
|
+
* Posts the review as a comment on the implementation PR.
|
|
735
|
+
* Returns the review result so callers can check the verdict.
|
|
736
|
+
*/
|
|
737
|
+
private async reviewImplementationPR(issueNumber: number): Promise<ReviewResult> {
|
|
738
|
+
console.log(`Reviewing implementation for issue #${issueNumber}...`);
|
|
739
|
+
return performReview(
|
|
740
|
+
{type: 'issue-tasks-completed', issueNumber},
|
|
741
|
+
{
|
|
742
|
+
workDir: this.config.workDir,
|
|
743
|
+
repo: this.config.repo,
|
|
744
|
+
logFile: this.config.logFile,
|
|
745
|
+
post: !this.config.noPush,
|
|
746
|
+
},
|
|
747
|
+
this.issues,
|
|
748
|
+
this.agent,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
512
751
|
}
|