@masslessai/push-todo 3.7.4 → 3.7.6

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/lib/config.js CHANGED
@@ -150,6 +150,48 @@ export function setAutoCommitEnabled(enabled) {
150
150
  return setConfigValue('AUTO_COMMIT', enabled ? 'true' : 'false');
151
151
  }
152
152
 
153
+ /**
154
+ * Check if auto-merge into main is enabled after daemon task completion.
155
+ * Default: true (merge PR into main after session_finished)
156
+ *
157
+ * @returns {boolean}
158
+ */
159
+ export function getAutoMergeEnabled() {
160
+ const value = getConfigValue('AUTO_MERGE', 'true');
161
+ return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
162
+ }
163
+
164
+ /**
165
+ * Set auto-merge setting.
166
+ *
167
+ * @param {boolean} enabled
168
+ * @returns {boolean} True if successful
169
+ */
170
+ export function setAutoMergeEnabled(enabled) {
171
+ return setConfigValue('AUTO_MERGE', enabled ? 'true' : 'false');
172
+ }
173
+
174
+ /**
175
+ * Check if auto-complete is enabled after daemon task merge.
176
+ * Default: true (mark task as completed after successful merge)
177
+ *
178
+ * @returns {boolean}
179
+ */
180
+ export function getAutoCompleteEnabled() {
181
+ const value = getConfigValue('AUTO_COMPLETE', 'true');
182
+ return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
183
+ }
184
+
185
+ /**
186
+ * Set auto-complete setting.
187
+ *
188
+ * @param {boolean} enabled
189
+ * @returns {boolean} True if successful
190
+ */
191
+ export function setAutoCompleteEnabled(enabled) {
192
+ return setConfigValue('AUTO_COMPLETE', enabled ? 'true' : 'false');
193
+ }
194
+
153
195
  /**
154
196
  * Get the maximum batch size for queuing tasks.
155
197
  * Default: 5
@@ -226,16 +268,24 @@ export function showSettings() {
226
268
  console.log();
227
269
 
228
270
  const autoCommit = getAutoCommitEnabled();
271
+ const autoMerge = getAutoMergeEnabled();
272
+ const autoComplete = getAutoCompleteEnabled();
229
273
  const batchSize = getMaxBatchSize();
230
274
 
231
- console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
232
- console.log(' Auto-commit when task completes');
275
+ console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
276
+ console.log(' Auto-commit when task completes');
277
+ console.log();
278
+ console.log(` auto-merge: ${autoMerge ? 'ON' : 'OFF'}`);
279
+ console.log(' Merge PR into main after daemon task finishes');
233
280
  console.log();
234
- console.log(` batch-size: ${batchSize}`);
235
- console.log(' Max tasks for batch queue (1-20)');
281
+ console.log(` auto-complete: ${autoComplete ? 'ON' : 'OFF'}`);
282
+ console.log(' Mark task completed after successful merge');
283
+ console.log();
284
+ console.log(` batch-size: ${batchSize}`);
285
+ console.log(' Max tasks for batch queue (1-20)');
236
286
  console.log();
237
287
  console.log(' Toggle with: push-todo setting <name>');
238
- console.log(' Example: push-todo setting auto-commit');
288
+ console.log(' Example: push-todo setting auto-merge');
239
289
  console.log();
240
290
  }
241
291
 
@@ -264,6 +314,38 @@ export function toggleSetting(settingName) {
264
314
  return false;
265
315
  }
266
316
 
317
+ if (normalized === 'auto-merge') {
318
+ const current = getAutoMergeEnabled();
319
+ const newValue = !current;
320
+ if (setAutoMergeEnabled(newValue)) {
321
+ console.log(`Auto-merge is now ${newValue ? 'ON' : 'OFF'}`);
322
+ if (newValue) {
323
+ console.log('PRs will be merged into main after daemon task finishes.');
324
+ } else {
325
+ console.log('PRs will NOT be merged automatically.');
326
+ }
327
+ return true;
328
+ }
329
+ console.error('Failed to update setting');
330
+ return false;
331
+ }
332
+
333
+ if (normalized === 'auto-complete') {
334
+ const current = getAutoCompleteEnabled();
335
+ const newValue = !current;
336
+ if (setAutoCompleteEnabled(newValue)) {
337
+ console.log(`Auto-complete is now ${newValue ? 'ON' : 'OFF'}`);
338
+ if (newValue) {
339
+ console.log('Tasks will be marked completed after successful merge.');
340
+ } else {
341
+ console.log('Tasks will NOT be marked completed automatically.');
342
+ }
343
+ return true;
344
+ }
345
+ console.error('Failed to update setting');
346
+ return false;
347
+ }
348
+
267
349
  if (normalized === 'batch-size') {
268
350
  const batchSize = getMaxBatchSize();
269
351
  console.log(`Current batch size: ${batchSize}`);
@@ -272,7 +354,7 @@ export function toggleSetting(settingName) {
272
354
  }
273
355
 
274
356
  console.error(`Unknown setting: ${settingName}`);
275
- console.error('Available settings: auto-commit, batch-size');
357
+ console.error('Available settings: auto-commit, auto-merge, auto-complete, batch-size');
276
358
  return false;
277
359
  }
278
360
 
package/lib/daemon.js CHANGED
@@ -199,6 +199,37 @@ function getVersion() {
199
199
  }
200
200
  }
201
201
 
202
+ function getConfigValueFromFile(key, defaultValue = '') {
203
+ const fullKey = `PUSH_${key}`;
204
+ if (!existsSync(CONFIG_FILE)) return defaultValue;
205
+ try {
206
+ const content = readFileSync(CONFIG_FILE, 'utf8');
207
+ for (const line of content.split('\n')) {
208
+ const trimmed = line.trim();
209
+ if (trimmed.startsWith(`export ${fullKey}=`)) {
210
+ let value = trimmed.split('=')[1] || '';
211
+ value = value.trim();
212
+ if ((value.startsWith('"') && value.endsWith('"')) ||
213
+ (value.startsWith("'") && value.endsWith("'"))) {
214
+ value = value.slice(1, -1);
215
+ }
216
+ return value;
217
+ }
218
+ }
219
+ } catch {}
220
+ return defaultValue;
221
+ }
222
+
223
+ function getAutoMergeEnabled() {
224
+ const v = getConfigValueFromFile('AUTO_MERGE', 'true');
225
+ return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
226
+ }
227
+
228
+ function getAutoCompleteEnabled() {
229
+ const v = getConfigValueFromFile('AUTO_COMPLETE', 'true');
230
+ return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
231
+ }
232
+
202
233
  // ==================== E2EE Decryption ====================
203
234
 
204
235
  let decryptTodoField = null;
@@ -378,11 +409,15 @@ async function claimTask(displayNumber) {
378
409
  return true;
379
410
  }
380
411
 
412
+ const suffix = getWorktreeSuffix();
413
+ const branch = `push-${displayNumber}-${suffix}`;
414
+
381
415
  const payload = {
382
416
  displayNumber,
383
417
  status: 'running',
384
418
  machineId,
385
419
  machineName,
420
+ branch,
386
421
  atomic: true
387
422
  };
388
423
 
@@ -596,6 +631,83 @@ Automated PR from Push daemon for task #${displayNumber}.
596
631
  }
597
632
  }
598
633
 
634
+ /**
635
+ * Merge a PR into main and update local main branch.
636
+ * Uses `gh pr merge` for clean remote merge, then pulls locally.
637
+ *
638
+ * @returns {boolean} True if merge succeeded
639
+ */
640
+ function mergePRForTask(displayNumber, prUrl, projectPath) {
641
+ const gitCwd = projectPath || process.cwd();
642
+
643
+ // Extract PR number from URL (e.g., https://github.com/user/repo/pull/42)
644
+ const prMatch = prUrl.match(/\/pull\/(\d+)/);
645
+ if (!prMatch) {
646
+ logError(`Could not extract PR number from: ${prUrl}`);
647
+ return false;
648
+ }
649
+ const prNumber = prMatch[1];
650
+
651
+ try {
652
+ execFileSync('gh', ['pr', 'merge', prNumber, '--merge', '--delete-branch'], {
653
+ cwd: gitCwd,
654
+ timeout: 60000,
655
+ stdio: 'pipe'
656
+ });
657
+ log(`Merged PR #${prNumber} for task #${displayNumber}`);
658
+
659
+ // Pull main locally so next worktree is up to date
660
+ try {
661
+ execFileSync('git', ['pull', '--ff-only'], {
662
+ cwd: gitCwd,
663
+ timeout: 30000,
664
+ stdio: 'pipe'
665
+ });
666
+ log('Updated local main branch');
667
+ } catch {
668
+ log('Could not pull main (may not be checked out), skipping local update');
669
+ }
670
+
671
+ return true;
672
+ } catch (e) {
673
+ const stderr = e.stderr?.toString() || e.message || '';
674
+ if (stderr.includes('merge conflict') || stderr.includes('conflict')) {
675
+ logError(`PR #${prNumber} has merge conflicts, skipping auto-merge`);
676
+ } else if (stderr.includes('not found') || stderr.includes('ENOENT')) {
677
+ log('GitHub CLI (gh) not installed, skipping merge');
678
+ } else {
679
+ logError(`Failed to merge PR #${prNumber}: ${stderr.slice(0, 200)}`);
680
+ }
681
+ return false;
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Mark a task as completed via the todo-status endpoint.
687
+ */
688
+ async function markTaskAsCompleted(displayNumber, taskId, comment) {
689
+ try {
690
+ const response = await apiRequest('todo-status', {
691
+ method: 'PATCH',
692
+ body: JSON.stringify({
693
+ todoId: taskId,
694
+ isCompleted: true,
695
+ completedAt: new Date().toISOString(),
696
+ completionComment: comment || `Completed by daemon on ${getMachineName()}`
697
+ })
698
+ });
699
+ if (response.ok) {
700
+ log(`Task #${displayNumber} marked as completed`);
701
+ return true;
702
+ }
703
+ logError(`Failed to mark #${displayNumber} complete: HTTP ${response.status}`);
704
+ return false;
705
+ } catch (error) {
706
+ logError(`Failed to mark #${displayNumber} complete: ${error.message}`);
707
+ return false;
708
+ }
709
+ }
710
+
599
711
  // ==================== Stuck Detection ====================
600
712
 
601
713
  function checkStuckPatterns(displayNumber, line) {
@@ -1000,12 +1112,27 @@ function handleTaskCompletion(displayNumber, exitCode) {
1000
1112
  );
1001
1113
  }
1002
1114
 
1115
+ // Auto-merge PR into main (configurable, default ON)
1116
+ let merged = false;
1117
+ if (getAutoMergeEnabled() && prUrl) {
1118
+ merged = mergePRForTask(displayNumber, prUrl, projectPath);
1119
+ }
1120
+
1121
+ // Auto-complete task after successful merge (configurable, default ON)
1122
+ const taskId = info.taskId;
1123
+ if (getAutoCompleteEnabled() && merged && taskId) {
1124
+ const comment = semanticSummary
1125
+ ? `${semanticSummary} (${durationStr} on ${machineName})`
1126
+ : `Completed in ${durationStr} on ${machineName}`;
1127
+ markTaskAsCompleted(displayNumber, taskId, comment);
1128
+ }
1129
+
1003
1130
  completedToday.push({
1004
1131
  displayNumber,
1005
1132
  summary,
1006
1133
  completedAt: new Date().toISOString(),
1007
1134
  duration,
1008
- status: 'session_finished',
1135
+ status: merged ? 'completed' : 'session_finished',
1009
1136
  prUrl,
1010
1137
  sessionId
1011
1138
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {