whitesmith 0.0.0 → 0.0.2
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 +228 -0
- package/dist/auto-work.d.ts +11 -0
- package/dist/auto-work.d.ts.map +1 -0
- package/dist/auto-work.js +22 -0
- package/dist/auto-work.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +108 -1
- package/dist/cli.js.map +1 -1
- package/dist/comment.d.ts +29 -0
- package/dist/comment.d.ts.map +1 -0
- package/dist/comment.js +390 -0
- package/dist/comment.js.map +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +57 -14
- package/dist/git.js.map +1 -1
- package/dist/harnesses/agent-harness.d.ts +13 -0
- package/dist/harnesses/agent-harness.d.ts.map +1 -1
- package/dist/harnesses/index.d.ts +1 -1
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/pi.d.ts +7 -5
- package/dist/harnesses/pi.d.ts.map +1 -1
- package/dist/harnesses/pi.js +122 -9
- package/dist/harnesses/pi.js.map +1 -1
- package/dist/install-ci.d.ts +7 -0
- package/dist/install-ci.d.ts.map +1 -0
- package/dist/install-ci.js +760 -0
- package/dist/install-ci.js.map +1 -0
- package/dist/orchestrator.d.ts +24 -4
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +254 -63
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +1 -0
- package/dist/prompts.js.map +1 -1
- package/dist/providers/github-ci.d.ts +16 -0
- package/dist/providers/github-ci.d.ts.map +1 -0
- package/dist/providers/github-ci.js +733 -0
- package/dist/providers/github-ci.js.map +1 -0
- package/dist/providers/github.d.ts +21 -0
- package/dist/providers/github.d.ts.map +1 -1
- package/dist/providers/github.js +88 -3
- package/dist/providers/github.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/issue-provider.d.ts +26 -0
- package/dist/providers/issue-provider.d.ts.map +1 -1
- package/dist/task-manager.d.ts +4 -0
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +6 -0
- package/dist/task-manager.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
- package/src/auto-work.ts +26 -0
- package/src/cli.ts +123 -1
- package/src/comment.ts +531 -0
- package/src/git.ts +58 -12
- package/src/harnesses/agent-harness.ts +15 -0
- package/src/harnesses/index.ts +1 -1
- package/src/harnesses/pi.ts +146 -10
- package/src/orchestrator.ts +290 -72
- package/src/prompts.ts +1 -0
- package/src/providers/github-ci.ts +840 -0
- package/src/providers/github.ts +118 -5
- package/src/providers/index.ts +1 -0
- package/src/providers/issue-provider.ts +25 -1
- package/src/task-manager.ts +7 -0
- package/src/types.ts +11 -0
package/src/orchestrator.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {AgentHarness} from './harnesses/agent-harness.js';
|
|
|
5
5
|
import {TaskManager} from './task-manager.js';
|
|
6
6
|
import {GitManager} from './git.js';
|
|
7
7
|
import {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
|
|
8
|
+
import {isAutoWorkEnabled} from './auto-work.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Main orchestrator for whitesmith.
|
|
@@ -12,7 +13,11 @@ import {buildInvestigatePrompt, buildImplementPrompt} from './prompts.js';
|
|
|
12
13
|
* The loop:
|
|
13
14
|
* 1. Reconcile — check if any issues with tasks-accepted have all tasks done
|
|
14
15
|
* 2. Investigate — pick an unlabeled issue, generate tasks
|
|
15
|
-
* 3. Implement — pick an available task, implement it
|
|
16
|
+
* 3. Implement — pick an available task, implement it on the issue/<number> branch
|
|
17
|
+
*
|
|
18
|
+
* Implementation uses a single branch per issue (`issue/<number>`). Each task
|
|
19
|
+
* adds one commit to the branch. When the last task completes, a PR is created
|
|
20
|
+
* immediately. `reconcile()` is a safety net for crash recovery.
|
|
16
21
|
*/
|
|
17
22
|
export class Orchestrator {
|
|
18
23
|
private config: DevPulseConfig;
|
|
@@ -37,10 +42,20 @@ export class Orchestrator {
|
|
|
37
42
|
console.log(`Working directory: ${this.config.workDir}`);
|
|
38
43
|
console.log(`Max iterations: ${this.config.maxIterations}`);
|
|
39
44
|
console.log(`Agent command: ${this.config.agentCmd}`);
|
|
45
|
+
console.log(`Provider: ${this.config.provider}`);
|
|
46
|
+
console.log(`Model: ${this.config.model}`);
|
|
40
47
|
console.log('');
|
|
41
48
|
|
|
42
|
-
//
|
|
43
|
-
|
|
49
|
+
// Skip agent validation and label creation in dry-run mode
|
|
50
|
+
if (!this.config.dryRun) {
|
|
51
|
+
// Validate agent is available before doing anything
|
|
52
|
+
await this.agent.validate();
|
|
53
|
+
console.log('Agent validated successfully.');
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
// Ensure labels exist
|
|
57
|
+
await this.issues.ensureLabels(Object.values(LABELS));
|
|
58
|
+
}
|
|
44
59
|
|
|
45
60
|
for (let i = 1; i <= this.config.maxIterations; i++) {
|
|
46
61
|
console.log('');
|
|
@@ -54,10 +69,38 @@ export class Orchestrator {
|
|
|
54
69
|
const action = await this.decideAction();
|
|
55
70
|
console.log(`Action: ${action.type}`);
|
|
56
71
|
|
|
72
|
+
if (this.config.dryRun) {
|
|
73
|
+
switch (action.type) {
|
|
74
|
+
case 'reconcile':
|
|
75
|
+
console.log(`Would reconcile issue #${action.issue.number}: ${action.issue.title}`);
|
|
76
|
+
break;
|
|
77
|
+
case 'auto-approve':
|
|
78
|
+
console.log(
|
|
79
|
+
`Would auto-approve task PR for issue #${action.issue.number}: ${action.issue.title}`,
|
|
80
|
+
);
|
|
81
|
+
break;
|
|
82
|
+
case 'investigate':
|
|
83
|
+
console.log(`Would investigate issue #${action.issue.number}: ${action.issue.title}`);
|
|
84
|
+
break;
|
|
85
|
+
case 'implement':
|
|
86
|
+
console.log(
|
|
87
|
+
`Would implement task ${action.task.id}: ${action.task.title} (issue #${action.issue.number})`,
|
|
88
|
+
);
|
|
89
|
+
break;
|
|
90
|
+
case 'idle':
|
|
91
|
+
console.log('Nothing to do. All issues are either in-progress or completed.');
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
switch (action.type) {
|
|
58
98
|
case 'reconcile':
|
|
59
99
|
await this.reconcile(action.issue);
|
|
60
100
|
break;
|
|
101
|
+
case 'auto-approve':
|
|
102
|
+
await this.autoApprove(action.issue);
|
|
103
|
+
break;
|
|
61
104
|
case 'investigate':
|
|
62
105
|
await this.investigate(action.issue);
|
|
63
106
|
break;
|
|
@@ -79,6 +122,20 @@ export class Orchestrator {
|
|
|
79
122
|
console.log('=== Iteration limit reached ===');
|
|
80
123
|
}
|
|
81
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Check whether all tasks for an issue have been completed on the issue branch.
|
|
127
|
+
* Works without checking out the branch by inspecting the remote via git ls-tree.
|
|
128
|
+
*/
|
|
129
|
+
private async allTasksCompletedOnBranch(issueNumber: number): Promise<boolean> {
|
|
130
|
+
const branch = `issue/${issueNumber}`;
|
|
131
|
+
const branchExists = await this.issues.remoteBranchExists(branch);
|
|
132
|
+
if (!branchExists) return false;
|
|
133
|
+
|
|
134
|
+
// Check if any task files remain on the issue branch
|
|
135
|
+
const hasFiles = await this.git.remotePathHasFiles(`origin/${branch}`, `tasks/${issueNumber}/`);
|
|
136
|
+
return !hasFiles;
|
|
137
|
+
}
|
|
138
|
+
|
|
82
139
|
/**
|
|
83
140
|
* Decide the next action to take
|
|
84
141
|
*/
|
|
@@ -86,18 +143,27 @@ export class Orchestrator {
|
|
|
86
143
|
// Priority 1: Reconcile — issues with tasks-accepted where all tasks are done
|
|
87
144
|
const acceptedIssues = await this.issues.listIssues({labels: [LABELS.TASKS_ACCEPTED]});
|
|
88
145
|
for (const issue of acceptedIssues) {
|
|
89
|
-
|
|
146
|
+
const allDone = await this.allTasksCompletedOnBranch(issue.number);
|
|
147
|
+
if (allDone) {
|
|
90
148
|
return {type: 'reconcile', issue};
|
|
91
149
|
}
|
|
92
150
|
}
|
|
93
151
|
|
|
94
|
-
// Priority 2:
|
|
152
|
+
// Priority 2: Auto-approve — merge task PRs for issues with auto-work enabled
|
|
153
|
+
const proposedIssues = await this.issues.listIssues({labels: [LABELS.TASKS_PROPOSED]});
|
|
154
|
+
for (const issue of proposedIssues) {
|
|
155
|
+
if (isAutoWorkEnabled(this.config, issue)) {
|
|
156
|
+
return {type: 'auto-approve' as const, issue};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Priority 3: Implement — find an available task
|
|
95
161
|
const implementAction = await this.findAvailableTask(acceptedIssues);
|
|
96
162
|
if (implementAction) {
|
|
97
163
|
return implementAction;
|
|
98
164
|
}
|
|
99
165
|
|
|
100
|
-
// Priority
|
|
166
|
+
// Priority 4: Investigate — find a new issue (no whitesmith labels)
|
|
101
167
|
const allDevPulseLabels = Object.values(LABELS);
|
|
102
168
|
const newIssues = await this.issues.listIssues({noLabels: allDevPulseLabels});
|
|
103
169
|
if (newIssues.length > 0) {
|
|
@@ -110,26 +176,47 @@ export class Orchestrator {
|
|
|
110
176
|
}
|
|
111
177
|
|
|
112
178
|
/**
|
|
113
|
-
* Find an available task to implement
|
|
179
|
+
* Find an available task to implement.
|
|
180
|
+
*
|
|
181
|
+
* Uses task files on `main` as the canonical list of pending tasks.
|
|
182
|
+
* If an issue branch exists, checks which task files have been deleted
|
|
183
|
+
* on it (= completed) and skips those.
|
|
114
184
|
*/
|
|
115
185
|
private async findAvailableTask(
|
|
116
186
|
acceptedIssues: Issue[],
|
|
117
187
|
): Promise<{type: 'implement'; task: Task; issue: Issue} | null> {
|
|
118
188
|
for (const issue of acceptedIssues) {
|
|
119
189
|
const issueTasks = this.tasks.listTasks(issue.number);
|
|
190
|
+
if (issueTasks.length === 0) continue;
|
|
191
|
+
|
|
192
|
+
// Determine which tasks are already completed on the issue branch
|
|
193
|
+
const branch = `issue/${issue.number}`;
|
|
194
|
+
const branchExists = await this.issues.remoteBranchExists(branch);
|
|
195
|
+
const completedTaskFiles = new Set<string>();
|
|
196
|
+
|
|
197
|
+
if (branchExists) {
|
|
198
|
+
// Check each task file's existence on the remote issue branch
|
|
199
|
+
for (const task of issueTasks) {
|
|
200
|
+
const existsOnBranch = await this.git.remoteFileExists(`origin/${branch}`, task.filePath);
|
|
201
|
+
if (!existsOnBranch) {
|
|
202
|
+
// Task file deleted on issue branch = completed
|
|
203
|
+
completedTaskFiles.add(task.filePath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
120
207
|
|
|
121
208
|
for (const task of issueTasks) {
|
|
122
|
-
//
|
|
123
|
-
if (
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
209
|
+
// Skip completed tasks
|
|
210
|
+
if (completedTaskFiles.has(task.filePath)) continue;
|
|
126
211
|
|
|
127
|
-
// Check
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
// Check dependencies are satisfied
|
|
213
|
+
// A dependency is satisfied if its task file is gone from main OR completed on the issue branch
|
|
214
|
+
const depsOk = task.dependsOn.every((depId) => {
|
|
215
|
+
const depTask = issueTasks.find((t) => t.id === depId);
|
|
216
|
+
if (!depTask) return true; // dep not in pending list on main = already merged
|
|
217
|
+
return completedTaskFiles.has(depTask.filePath);
|
|
218
|
+
});
|
|
219
|
+
if (!depsOk) continue;
|
|
133
220
|
|
|
134
221
|
return {type: 'implement', task, issue};
|
|
135
222
|
}
|
|
@@ -139,12 +226,36 @@ export class Orchestrator {
|
|
|
139
226
|
}
|
|
140
227
|
|
|
141
228
|
/**
|
|
142
|
-
* Phase 1: Reconcile — mark issue as completed, close it
|
|
229
|
+
* Phase 1: Reconcile — mark issue as completed, close it.
|
|
230
|
+
* Also serves as safety net: creates PR if all tasks are done but no PR exists
|
|
231
|
+
* (e.g. agent crashed after last task push but before PR creation).
|
|
143
232
|
*/
|
|
144
233
|
private async reconcile(issue: Issue): Promise<void> {
|
|
145
234
|
console.log(`Reconciling issue #${issue.number}: ${issue.title}`);
|
|
146
235
|
console.log('All tasks completed. Marking issue as done.');
|
|
147
236
|
|
|
237
|
+
// Safety net: ensure a PR exists for the issue branch
|
|
238
|
+
const branch = `issue/${issue.number}`;
|
|
239
|
+
const branchExists = await this.issues.remoteBranchExists(branch);
|
|
240
|
+
if (branchExists && !this.config.noPush) {
|
|
241
|
+
const existingPR = await this.issues.getPRForBranch(branch);
|
|
242
|
+
if (!existingPR) {
|
|
243
|
+
console.log(`Safety net: creating PR for ${branch} (missed during implement)`);
|
|
244
|
+
const issueTasks = this.tasks.listTasks(issue.number);
|
|
245
|
+
const taskSummary =
|
|
246
|
+
issueTasks.length > 0
|
|
247
|
+
? issueTasks.map((t) => `- ✅ **${t.id}**: ${t.title}`).join('\n')
|
|
248
|
+
: '- All tasks completed';
|
|
249
|
+
const prUrl = await this.issues.createPR({
|
|
250
|
+
head: branch,
|
|
251
|
+
base: 'main',
|
|
252
|
+
title: `feat(#${issue.number}): ${issue.title}`,
|
|
253
|
+
body: `## Implementation for #${issue.number}\n\n${taskSummary}\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number}`,
|
|
254
|
+
});
|
|
255
|
+
console.log(`Safety net PR created: ${prUrl}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
148
259
|
await this.issues.addLabel(issue.number, LABELS.COMPLETED);
|
|
149
260
|
await this.issues.removeLabel(issue.number, LABELS.TASKS_ACCEPTED);
|
|
150
261
|
await this.issues.comment(
|
|
@@ -156,6 +267,33 @@ export class Orchestrator {
|
|
|
156
267
|
console.log(`Issue #${issue.number} closed.`);
|
|
157
268
|
}
|
|
158
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Phase 1.5: Auto-approve — merge the task-proposal PR when auto-work is enabled
|
|
272
|
+
*/
|
|
273
|
+
private async autoApprove(issue: Issue): Promise<void> {
|
|
274
|
+
console.log(`Auto-approving task PR for issue #${issue.number}: ${issue.title}`);
|
|
275
|
+
|
|
276
|
+
const branch = `investigate/${issue.number}`;
|
|
277
|
+
const pr = await this.issues.getPRForBranch(branch);
|
|
278
|
+
|
|
279
|
+
if (!pr || pr.state !== 'open') {
|
|
280
|
+
console.log(`No open PR found for branch '${branch}', skipping auto-approve`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await this.issues.mergePR(pr.number);
|
|
285
|
+
console.log(`Merged PR #${pr.number}: ${pr.url}`);
|
|
286
|
+
|
|
287
|
+
await this.issues.removeLabel(issue.number, LABELS.TASKS_PROPOSED);
|
|
288
|
+
await this.issues.addLabel(issue.number, LABELS.TASKS_ACCEPTED);
|
|
289
|
+
await this.issues.comment(
|
|
290
|
+
issue.number,
|
|
291
|
+
`🤖 Task PR #${pr.number} has been auto-approved and merged. Tasks are now on \`main\`.`,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
console.log(`Issue #${issue.number} transitioned to tasks-accepted.`);
|
|
295
|
+
}
|
|
296
|
+
|
|
159
297
|
/**
|
|
160
298
|
* Phase 2: Investigate — generate tasks for a new issue
|
|
161
299
|
*/
|
|
@@ -169,23 +307,47 @@ export class Orchestrator {
|
|
|
169
307
|
const issueTasksDir = `tasks/${issue.number}`;
|
|
170
308
|
|
|
171
309
|
try {
|
|
172
|
-
//
|
|
310
|
+
// Check if a previous attempt already produced work on this branch
|
|
311
|
+
const remoteBranchExists = await this.issues.remoteBranchExists(branch);
|
|
312
|
+
let agentNeeded = true;
|
|
313
|
+
|
|
173
314
|
await this.git.deleteLocalBranch(branch);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
315
|
+
|
|
316
|
+
if (remoteBranchExists) {
|
|
317
|
+
// Checkout the existing remote branch to inspect it
|
|
318
|
+
await this.git.checkout(branch, {create: true, startPoint: `origin/${branch}`});
|
|
319
|
+
const existingTasks = this.tasks.listTasks(issue.number);
|
|
320
|
+
if (existingTasks.length > 0) {
|
|
321
|
+
// Previous attempt completed the work — skip the agent
|
|
322
|
+
console.log(
|
|
323
|
+
`Branch '${branch}' already exists with ${existingTasks.length} task(s), skipping agent`,
|
|
324
|
+
);
|
|
325
|
+
agentNeeded = false;
|
|
326
|
+
} else {
|
|
327
|
+
// Branch exists but no task files — start fresh
|
|
328
|
+
console.log(`Branch '${branch}' exists but has no tasks, starting fresh`);
|
|
329
|
+
await this.git.deleteLocalBranch(branch);
|
|
330
|
+
await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (agentNeeded) {
|
|
337
|
+
// Run agent to generate tasks
|
|
338
|
+
const prompt = buildInvestigatePrompt(issue, issueTasksDir);
|
|
339
|
+
const {exitCode} = await this.agent.run({
|
|
340
|
+
prompt,
|
|
341
|
+
workDir: this.config.workDir,
|
|
342
|
+
logFile: this.config.logFile,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (exitCode !== 0) {
|
|
346
|
+
console.error(`Agent failed with exit code ${exitCode}`);
|
|
347
|
+
await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
|
|
348
|
+
await this.git.checkoutMain();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
189
351
|
}
|
|
190
352
|
|
|
191
353
|
// Verify task files were created
|
|
@@ -206,16 +368,25 @@ export class Orchestrator {
|
|
|
206
368
|
if (this.config.noPush) {
|
|
207
369
|
console.log(`Branch '${branch}' ready (--no-push mode)`);
|
|
208
370
|
} else {
|
|
209
|
-
//
|
|
210
|
-
await this.git.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
371
|
+
// Force push since the branch may exist from a previous failed attempt
|
|
372
|
+
await this.git.forcePush(branch);
|
|
373
|
+
|
|
374
|
+
// Check if a PR already exists for this branch
|
|
375
|
+
const existingPR = await this.issues.getPRForBranch(branch);
|
|
376
|
+
let prUrl: string;
|
|
377
|
+
|
|
378
|
+
if (existingPR && existingPR.state === 'open') {
|
|
379
|
+
prUrl = existingPR.url;
|
|
380
|
+
console.log(`PR already exists: ${prUrl}`);
|
|
381
|
+
} else {
|
|
382
|
+
const taskList = tasks.map((t) => `- [ ] **${t.id}**: ${t.title}`).join('\n');
|
|
383
|
+
prUrl = await this.issues.createPR({
|
|
384
|
+
head: branch,
|
|
385
|
+
base: 'main',
|
|
386
|
+
title: `tasks(#${issue.number}): ${issue.title}`,
|
|
387
|
+
body: `## Generated Tasks for #${issue.number}\n\n${taskList}\n\n---\n*Generated by whitesmith from issue #${issue.number}*`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
219
390
|
|
|
220
391
|
await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
|
|
221
392
|
await this.issues.addLabel(issue.number, LABELS.TASKS_PROPOSED);
|
|
@@ -236,31 +407,60 @@ export class Orchestrator {
|
|
|
236
407
|
}
|
|
237
408
|
|
|
238
409
|
/**
|
|
239
|
-
* Phase 3: Implement — implement a task
|
|
410
|
+
* Phase 3: Implement — implement a task on the issue/<number> branch.
|
|
411
|
+
* Each task adds one commit. When all tasks are done, a PR is created immediately.
|
|
240
412
|
*/
|
|
241
413
|
private async implement(task: Task, issue: Issue): Promise<void> {
|
|
242
414
|
console.log(`Implementing task ${task.id}: ${task.title}`);
|
|
243
415
|
console.log(`For issue #${issue.number}: ${issue.title}`);
|
|
244
416
|
|
|
245
|
-
const branch = `
|
|
417
|
+
const branch = `issue/${issue.number}`;
|
|
246
418
|
|
|
247
419
|
try {
|
|
248
|
-
//
|
|
420
|
+
// Check if the issue branch already exists (previous tasks may have committed to it)
|
|
421
|
+
const remoteBranchExists = await this.issues.remoteBranchExists(branch);
|
|
422
|
+
let agentNeeded = true;
|
|
423
|
+
|
|
249
424
|
await this.git.deleteLocalBranch(branch);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
425
|
+
|
|
426
|
+
if (remoteBranchExists) {
|
|
427
|
+
// Continue from existing issue branch (accumulate commits)
|
|
428
|
+
await this.git.checkout(branch, {create: true, startPoint: `origin/${branch}`});
|
|
429
|
+
|
|
430
|
+
// Check if this specific task was already completed on the branch
|
|
431
|
+
const taskFileExists = this.tasks.taskFileExists(task.filePath);
|
|
432
|
+
if (!taskFileExists) {
|
|
433
|
+
console.log(
|
|
434
|
+
`Task file '${task.filePath}' already deleted on branch '${branch}', skipping agent`,
|
|
435
|
+
);
|
|
436
|
+
agentNeeded = false;
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (agentNeeded) {
|
|
443
|
+
const prompt = buildImplementPrompt(task, issue);
|
|
444
|
+
const {exitCode} = await this.agent.run({
|
|
445
|
+
prompt,
|
|
446
|
+
workDir: this.config.workDir,
|
|
447
|
+
logFile: this.config.logFile,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (exitCode !== 0) {
|
|
451
|
+
console.error(`Agent failed with exit code ${exitCode}`);
|
|
452
|
+
await this.git.checkoutMain();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Verify the agent actually deleted the task file
|
|
457
|
+
if (this.tasks.taskFileExists(task.filePath)) {
|
|
458
|
+
console.error(
|
|
459
|
+
`Agent exited successfully but did not delete task file '${task.filePath}'. Treating as incomplete.`,
|
|
460
|
+
);
|
|
461
|
+
await this.git.checkoutMain();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
264
464
|
}
|
|
265
465
|
|
|
266
466
|
// Verify we're still on the right branch
|
|
@@ -272,17 +472,35 @@ export class Orchestrator {
|
|
|
272
472
|
if (this.config.noPush) {
|
|
273
473
|
console.log(`Branch '${branch}' ready (--no-push mode)`);
|
|
274
474
|
} else {
|
|
275
|
-
//
|
|
276
|
-
await this.git.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
475
|
+
// Force push since the branch may exist from a previous failed attempt
|
|
476
|
+
await this.git.forcePush(branch);
|
|
477
|
+
|
|
478
|
+
// Check if all tasks for this issue are now complete
|
|
479
|
+
// (task files deleted on the current working tree = issue branch)
|
|
480
|
+
const remainingTasks = this.tasks.listTasks(issue.number);
|
|
481
|
+
if (remainingTasks.length === 0) {
|
|
482
|
+
// All tasks done — create PR immediately
|
|
483
|
+
const existingPR = await this.issues.getPRForBranch(branch);
|
|
484
|
+
let prUrl: string;
|
|
485
|
+
|
|
486
|
+
if (existingPR && existingPR.state === 'open') {
|
|
487
|
+
prUrl = existingPR.url;
|
|
488
|
+
console.log(`PR already exists: ${prUrl}`);
|
|
489
|
+
} else {
|
|
490
|
+
prUrl = await this.issues.createPR({
|
|
491
|
+
head: branch,
|
|
492
|
+
base: 'main',
|
|
493
|
+
title: `feat(#${issue.number}): ${issue.title}`,
|
|
494
|
+
body: `## Implementation for #${issue.number}\n\nAll tasks completed.\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number}`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
console.log(`PR created: ${prUrl}`);
|
|
499
|
+
} else {
|
|
500
|
+
console.log(
|
|
501
|
+
`Task ${task.id} committed. ${remainingTasks.length} task(s) remaining for issue #${issue.number}.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
286
504
|
}
|
|
287
505
|
} catch (error) {
|
|
288
506
|
console.error('Implementation failed:', error instanceof Error ? error.message : error);
|
package/src/prompts.ts
CHANGED
|
@@ -110,5 +110,6 @@ git commit -m "feat(#${issue.number}): ${task.title}"
|
|
|
110
110
|
- Do NOT modify other task files.
|
|
111
111
|
- You MUST delete \`${task.filePath}\` as part of your commit.
|
|
112
112
|
- If the \`tasks/${task.issue}/\` directory is empty after deletion, remove it.
|
|
113
|
+
- **Always use tool calls to make changes.** Never just describe what you plan to do — actually do it. If you produce a response with no tool calls, the session ends immediately and your work is lost.
|
|
113
114
|
`;
|
|
114
115
|
}
|