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.
Files changed (71) hide show
  1. package/README.md +228 -0
  2. package/dist/auto-work.d.ts +11 -0
  3. package/dist/auto-work.d.ts.map +1 -0
  4. package/dist/auto-work.js +22 -0
  5. package/dist/auto-work.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +108 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/comment.d.ts +29 -0
  10. package/dist/comment.d.ts.map +1 -0
  11. package/dist/comment.js +390 -0
  12. package/dist/comment.js.map +1 -0
  13. package/dist/git.d.ts +12 -0
  14. package/dist/git.d.ts.map +1 -1
  15. package/dist/git.js +57 -14
  16. package/dist/git.js.map +1 -1
  17. package/dist/harnesses/agent-harness.d.ts +13 -0
  18. package/dist/harnesses/agent-harness.d.ts.map +1 -1
  19. package/dist/harnesses/index.d.ts +1 -1
  20. package/dist/harnesses/index.d.ts.map +1 -1
  21. package/dist/harnesses/pi.d.ts +7 -5
  22. package/dist/harnesses/pi.d.ts.map +1 -1
  23. package/dist/harnesses/pi.js +122 -9
  24. package/dist/harnesses/pi.js.map +1 -1
  25. package/dist/install-ci.d.ts +7 -0
  26. package/dist/install-ci.d.ts.map +1 -0
  27. package/dist/install-ci.js +760 -0
  28. package/dist/install-ci.js.map +1 -0
  29. package/dist/orchestrator.d.ts +24 -4
  30. package/dist/orchestrator.d.ts.map +1 -1
  31. package/dist/orchestrator.js +254 -63
  32. package/dist/orchestrator.js.map +1 -1
  33. package/dist/prompts.d.ts.map +1 -1
  34. package/dist/prompts.js +1 -0
  35. package/dist/prompts.js.map +1 -1
  36. package/dist/providers/github-ci.d.ts +16 -0
  37. package/dist/providers/github-ci.d.ts.map +1 -0
  38. package/dist/providers/github-ci.js +733 -0
  39. package/dist/providers/github-ci.js.map +1 -0
  40. package/dist/providers/github.d.ts +21 -0
  41. package/dist/providers/github.d.ts.map +1 -1
  42. package/dist/providers/github.js +88 -3
  43. package/dist/providers/github.js.map +1 -1
  44. package/dist/providers/index.d.ts +1 -0
  45. package/dist/providers/index.d.ts.map +1 -1
  46. package/dist/providers/issue-provider.d.ts +26 -0
  47. package/dist/providers/issue-provider.d.ts.map +1 -1
  48. package/dist/task-manager.d.ts +4 -0
  49. package/dist/task-manager.d.ts.map +1 -1
  50. package/dist/task-manager.js +6 -0
  51. package/dist/task-manager.js.map +1 -1
  52. package/dist/types.d.ts +13 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +2 -0
  55. package/dist/types.js.map +1 -1
  56. package/package.json +3 -1
  57. package/src/auto-work.ts +26 -0
  58. package/src/cli.ts +123 -1
  59. package/src/comment.ts +531 -0
  60. package/src/git.ts +58 -12
  61. package/src/harnesses/agent-harness.ts +15 -0
  62. package/src/harnesses/index.ts +1 -1
  63. package/src/harnesses/pi.ts +146 -10
  64. package/src/orchestrator.ts +290 -72
  65. package/src/prompts.ts +1 -0
  66. package/src/providers/github-ci.ts +840 -0
  67. package/src/providers/github.ts +118 -5
  68. package/src/providers/index.ts +1 -0
  69. package/src/providers/issue-provider.ts +25 -1
  70. package/src/task-manager.ts +7 -0
  71. package/src/types.ts +11 -0
@@ -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
- // Ensure labels exist
43
- await this.issues.ensureLabels(Object.values(LABELS));
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
- if (!this.tasks.hasRemainingTasks(issue.number)) {
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: Implementfind an available task
152
+ // Priority 2: Auto-approvemerge 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 3: Investigate — find a new issue (no whitesmith labels)
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
- // Check dependencies are satisfied
123
- if (!this.tasks.areDependenciesSatisfied(task)) {
124
- continue;
125
- }
209
+ // Skip completed tasks
210
+ if (completedTaskFiles.has(task.filePath)) continue;
126
211
 
127
- // Check if someone is already working on it (branch exists)
128
- const branch = `task/${task.id}`;
129
- const branchExists = await this.issues.remoteBranchExists(branch);
130
- if (branchExists) {
131
- continue;
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
- // Create branch from main
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
- await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
175
-
176
- // Run agent to generate tasks
177
- const prompt = buildInvestigatePrompt(issue, issueTasksDir);
178
- const {exitCode} = await this.agent.run({
179
- prompt,
180
- workDir: this.config.workDir,
181
- logFile: this.config.logFile,
182
- });
183
-
184
- if (exitCode !== 0) {
185
- console.error(`Agent failed with exit code ${exitCode}`);
186
- await this.issues.removeLabel(issue.number, LABELS.INVESTIGATING);
187
- await this.git.checkoutMain();
188
- return;
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
- // Push and create PR
210
- await this.git.push(branch);
211
-
212
- const taskList = tasks.map((t) => `- [ ] **${t.id}**: ${t.title}`).join('\n');
213
- const prUrl = await this.issues.createPR({
214
- head: branch,
215
- base: 'main',
216
- title: `tasks(#${issue.number}): ${issue.title}`,
217
- body: `## Generated Tasks for #${issue.number}\n\n${taskList}\n\n---\n*Generated by whitesmith from issue #${issue.number}*`,
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 and create a PR
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 = `task/${task.id}`;
417
+ const branch = `issue/${issue.number}`;
246
418
 
247
419
  try {
248
- // Create branch from main
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
- await this.git.checkout(branch, {create: true, startPoint: 'origin/main'});
251
-
252
- // Run agent to implement
253
- const prompt = buildImplementPrompt(task, issue);
254
- const {exitCode} = await this.agent.run({
255
- prompt,
256
- workDir: this.config.workDir,
257
- logFile: this.config.logFile,
258
- });
259
-
260
- if (exitCode !== 0) {
261
- console.error(`Agent failed with exit code ${exitCode}`);
262
- await this.git.checkoutMain();
263
- return;
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
- // Push and create PR
276
- await this.git.push(branch);
277
-
278
- const prUrl = await this.issues.createPR({
279
- head: branch,
280
- base: 'main',
281
- title: `feat(#${issue.number}): ${task.title}`,
282
- body: `## Task: ${task.title}\n\nImplements task \`${task.id}\` from issue #${issue.number}.\n\n### From the task spec:\n\n${task.content}\n\n---\n*Implemented by whitesmith*\n\nCloses #${issue.number} (if all tasks are complete)`,
283
- });
284
-
285
- console.log(`PR created: ${prUrl}`);
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
  }