@masslessai/push-todo 3.7.8 → 3.7.9
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/daemon.js +154 -15
- package/lib/fetch.js +10 -1
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -686,15 +686,13 @@ Automated PR from Push daemon for task #${displayNumber}.
|
|
|
686
686
|
}
|
|
687
687
|
|
|
688
688
|
/**
|
|
689
|
-
*
|
|
690
|
-
* Uses `gh pr merge` for clean remote merge, then pulls locally.
|
|
689
|
+
* Attempt to merge a PR. If merge conflicts, use Claude to resolve them.
|
|
691
690
|
*
|
|
692
691
|
* @returns {boolean} True if merge succeeded
|
|
693
692
|
*/
|
|
694
693
|
function mergePRForTask(displayNumber, prUrl, projectPath) {
|
|
695
694
|
const gitCwd = projectPath || process.cwd();
|
|
696
695
|
|
|
697
|
-
// Extract PR number from URL (e.g., https://github.com/user/repo/pull/42)
|
|
698
696
|
const prMatch = prUrl.match(/\/pull\/(\d+)/);
|
|
699
697
|
if (!prMatch) {
|
|
700
698
|
logError(`Could not extract PR number from: ${prUrl}`);
|
|
@@ -702,6 +700,26 @@ function mergePRForTask(displayNumber, prUrl, projectPath) {
|
|
|
702
700
|
}
|
|
703
701
|
const prNumber = prMatch[1];
|
|
704
702
|
|
|
703
|
+
// First attempt: direct merge
|
|
704
|
+
const firstResult = attemptPRMerge(prNumber, displayNumber, gitCwd);
|
|
705
|
+
if (firstResult === 'success') return true;
|
|
706
|
+
if (firstResult !== 'conflict') return false; // non-conflict failure
|
|
707
|
+
|
|
708
|
+
// Conflict detected — try to resolve with Claude
|
|
709
|
+
const suffix = getWorktreeSuffix();
|
|
710
|
+
const branch = `push-${displayNumber}-${suffix}`;
|
|
711
|
+
const resolved = resolveConflictsWithClaude(displayNumber, branch, gitCwd);
|
|
712
|
+
if (!resolved) return false;
|
|
713
|
+
|
|
714
|
+
// Second attempt after conflict resolution
|
|
715
|
+
log(`Retrying merge for PR #${prNumber} after conflict resolution`);
|
|
716
|
+
return attemptPRMerge(prNumber, displayNumber, gitCwd) === 'success';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Try gh pr merge. Returns 'success', 'conflict', or 'error'.
|
|
721
|
+
*/
|
|
722
|
+
function attemptPRMerge(prNumber, displayNumber, gitCwd) {
|
|
705
723
|
try {
|
|
706
724
|
execFileSync('gh', ['pr', 'merge', prNumber, '--merge', '--delete-branch'], {
|
|
707
725
|
cwd: gitCwd,
|
|
@@ -722,16 +740,132 @@ function mergePRForTask(displayNumber, prUrl, projectPath) {
|
|
|
722
740
|
log('Could not pull main (may not be checked out), skipping local update');
|
|
723
741
|
}
|
|
724
742
|
|
|
725
|
-
return
|
|
743
|
+
return 'success';
|
|
726
744
|
} catch (e) {
|
|
727
745
|
const stderr = e.stderr?.toString() || e.message || '';
|
|
728
|
-
if (stderr.includes('
|
|
729
|
-
logError(`PR #${prNumber} has merge conflicts, skipping auto-merge`);
|
|
730
|
-
} else if (stderr.includes('not found') || stderr.includes('ENOENT')) {
|
|
746
|
+
if (stderr.includes('not found') || stderr.includes('ENOENT')) {
|
|
731
747
|
log('GitHub CLI (gh) not installed, skipping merge');
|
|
732
|
-
|
|
733
|
-
|
|
748
|
+
return 'error';
|
|
749
|
+
}
|
|
750
|
+
if (stderr.includes('merge conflict') || stderr.includes('conflict')) {
|
|
751
|
+
log(`PR #${prNumber} has merge conflicts, will attempt resolution`);
|
|
752
|
+
return 'conflict';
|
|
753
|
+
}
|
|
754
|
+
logError(`PR merge failed for #${displayNumber}: ${stderr.slice(0, 200)}`);
|
|
755
|
+
return 'error';
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Resolve merge conflicts by creating a worktree, merging main, using Claude
|
|
761
|
+
* to fix conflicts, then committing and pushing the resolution.
|
|
762
|
+
*
|
|
763
|
+
* @returns {boolean} True if conflicts were resolved and pushed
|
|
764
|
+
*/
|
|
765
|
+
function resolveConflictsWithClaude(displayNumber, branch, projectPath) {
|
|
766
|
+
log(`Attempting conflict resolution for task #${displayNumber}`);
|
|
767
|
+
|
|
768
|
+
const worktreePath = getWorktreePath(displayNumber, projectPath);
|
|
769
|
+
|
|
770
|
+
// Recreate worktree from the existing branch
|
|
771
|
+
try {
|
|
772
|
+
if (!existsSync(worktreePath)) {
|
|
773
|
+
execFileSync('git', ['worktree', 'add', worktreePath, branch], {
|
|
774
|
+
cwd: projectPath,
|
|
775
|
+
timeout: 30000,
|
|
776
|
+
stdio: 'pipe'
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
} catch (e) {
|
|
780
|
+
logError(`Could not create worktree for conflict resolution: ${e.message}`);
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
// Fetch latest main
|
|
786
|
+
execFileSync('git', ['fetch', 'origin'], {
|
|
787
|
+
cwd: worktreePath,
|
|
788
|
+
timeout: 30000,
|
|
789
|
+
stdio: 'pipe'
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Attempt merge — this will create conflict markers if conflicts exist
|
|
793
|
+
try {
|
|
794
|
+
execFileSync('git', ['merge', 'origin/main', '--no-edit'], {
|
|
795
|
+
cwd: worktreePath,
|
|
796
|
+
timeout: 30000,
|
|
797
|
+
stdio: 'pipe'
|
|
798
|
+
});
|
|
799
|
+
// No conflicts — merge succeeded cleanly
|
|
800
|
+
log(`No actual conflicts for #${displayNumber}, merge succeeded locally`);
|
|
801
|
+
execFileSync('git', ['push', 'origin', branch], {
|
|
802
|
+
cwd: worktreePath,
|
|
803
|
+
timeout: 30000,
|
|
804
|
+
stdio: 'pipe'
|
|
805
|
+
});
|
|
806
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
807
|
+
return true;
|
|
808
|
+
} catch {
|
|
809
|
+
// Expected: merge conflicts exist, continue to resolution
|
|
810
|
+
log(`Conflicts detected for #${displayNumber}, invoking Claude to resolve`);
|
|
734
811
|
}
|
|
812
|
+
|
|
813
|
+
// Use Claude to resolve the conflicts
|
|
814
|
+
const prompt = [
|
|
815
|
+
'There are merge conflicts in this repository from merging origin/main.',
|
|
816
|
+
'Run `git diff` to see the conflict markers.',
|
|
817
|
+
'Resolve ALL conflicts by choosing the correct code for each one.',
|
|
818
|
+
'After resolving, stage the files with `git add` and commit with:',
|
|
819
|
+
`git commit -m "Resolve merge conflicts for task #${displayNumber}"`,
|
|
820
|
+
'Do NOT push. Just resolve and commit.'
|
|
821
|
+
].join(' ');
|
|
822
|
+
|
|
823
|
+
execFileSync('claude', [
|
|
824
|
+
'-p', prompt,
|
|
825
|
+
'--allowedTools', 'Bash(git *),Read,Edit,Write,Glob,Grep',
|
|
826
|
+
'--output-format', 'json',
|
|
827
|
+
'--permission-mode', 'bypassPermissions'
|
|
828
|
+
], {
|
|
829
|
+
cwd: worktreePath,
|
|
830
|
+
timeout: 120000, // 2 min for conflict resolution
|
|
831
|
+
stdio: 'pipe'
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Verify the merge completed (no unmerged files remaining)
|
|
835
|
+
try {
|
|
836
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
837
|
+
cwd: worktreePath,
|
|
838
|
+
timeout: 10000,
|
|
839
|
+
encoding: 'utf8',
|
|
840
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
841
|
+
}).toString();
|
|
842
|
+
|
|
843
|
+
if (status.includes('UU ') || status.includes('AA ') || status.includes('DD ')) {
|
|
844
|
+
logError(`Conflicts not fully resolved for #${displayNumber}`);
|
|
845
|
+
try { execFileSync('git', ['merge', '--abort'], { cwd: worktreePath, stdio: 'pipe' }); } catch {}
|
|
846
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
try { execFileSync('git', ['merge', '--abort'], { cwd: worktreePath, stdio: 'pipe' }); } catch {}
|
|
851
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Push the resolved branch
|
|
856
|
+
execFileSync('git', ['push', 'origin', branch], {
|
|
857
|
+
cwd: worktreePath,
|
|
858
|
+
timeout: 30000,
|
|
859
|
+
stdio: 'pipe'
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
log(`Conflict resolution pushed for task #${displayNumber}`);
|
|
863
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
864
|
+
return true;
|
|
865
|
+
} catch (e) {
|
|
866
|
+
logError(`Conflict resolution failed for #${displayNumber}: ${e.message}`);
|
|
867
|
+
try { execFileSync('git', ['merge', '--abort'], { cwd: worktreePath, stdio: 'pipe' }); } catch {}
|
|
868
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
735
869
|
return false;
|
|
736
870
|
}
|
|
737
871
|
}
|
|
@@ -1243,9 +1377,14 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1243
1377
|
// Auto-create PR first so we can include it in the summary
|
|
1244
1378
|
const prUrl = createPRForTask(displayNumber, summary, projectPath);
|
|
1245
1379
|
|
|
1246
|
-
// Ask Claude to summarize what it accomplished
|
|
1380
|
+
// Ask Claude to summarize what it accomplished (needs worktree path)
|
|
1247
1381
|
const semanticSummary = extractSemanticSummary(worktreePath, sessionId);
|
|
1248
1382
|
|
|
1383
|
+
// Clean up worktree BEFORE merge — gh pr merge --delete-branch fails if
|
|
1384
|
+
// the local branch is still referenced by a worktree. The branch itself
|
|
1385
|
+
// is preserved (only the worktree checkout is removed).
|
|
1386
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
1387
|
+
|
|
1249
1388
|
// Combine: semantic summary first (what), then machine metadata (how)
|
|
1250
1389
|
let executionSummary = '';
|
|
1251
1390
|
if (semanticSummary) {
|
|
@@ -1308,8 +1447,12 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1308
1447
|
} else {
|
|
1309
1448
|
const stderr = taskInfo.process.stderr?.read()?.toString() || '';
|
|
1310
1449
|
|
|
1311
|
-
// Ask Claude to explain what went wrong (
|
|
1450
|
+
// Ask Claude to explain what went wrong (needs worktree path)
|
|
1312
1451
|
const failureSummary = extractSemanticSummary(worktreePath, sessionId);
|
|
1452
|
+
|
|
1453
|
+
// Clean up worktree after summary extraction
|
|
1454
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
1455
|
+
|
|
1313
1456
|
const errorMsg = failureSummary
|
|
1314
1457
|
? `${failureSummary}\nExit code ${exitCode}. Ran for ${durationStr} on ${machineName}.`
|
|
1315
1458
|
: `Exit code ${exitCode}: ${stderr.slice(0, 200)}`;
|
|
@@ -1338,10 +1481,6 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1338
1481
|
taskLastOutput.delete(displayNumber);
|
|
1339
1482
|
taskStdoutBuffer.delete(displayNumber);
|
|
1340
1483
|
taskProjectPaths.delete(displayNumber);
|
|
1341
|
-
|
|
1342
|
-
// Always clean up worktree — the branch preserves all committed work.
|
|
1343
|
-
// On re-run, createWorktree() recreates from the existing branch.
|
|
1344
|
-
cleanupWorktree(displayNumber, projectPath);
|
|
1345
1484
|
updateStatusFile();
|
|
1346
1485
|
}
|
|
1347
1486
|
|
package/lib/fetch.js
CHANGED
|
@@ -11,7 +11,7 @@ import { getGitRemote, isGitRepo } from './utils/git.js';
|
|
|
11
11
|
import { formatTaskForDisplay, formatSearchResult } from './utils/format.js';
|
|
12
12
|
import { bold, green, yellow, red, cyan, dim, muted } from './utils/colors.js';
|
|
13
13
|
import { decryptTodoField, isE2EEAvailable } from './encryption.js';
|
|
14
|
-
import { getAutoCommitEnabled, getMaxBatchSize } from './config.js';
|
|
14
|
+
import { getAutoCommitEnabled, getAutoMergeEnabled, getAutoCompleteEnabled, getAutoUpdateEnabled, getMaxBatchSize } from './config.js';
|
|
15
15
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
16
16
|
import { homedir } from 'os';
|
|
17
17
|
import { join } from 'path';
|
|
@@ -333,6 +333,9 @@ export async function showStatus(options = {}) {
|
|
|
333
333
|
const registry = getRegistry();
|
|
334
334
|
const [e2eeAvailable, e2eeMessage] = isE2EEAvailable();
|
|
335
335
|
const autoCommit = getAutoCommitEnabled();
|
|
336
|
+
const autoMerge = getAutoMergeEnabled();
|
|
337
|
+
const autoComplete = getAutoCompleteEnabled();
|
|
338
|
+
const autoUpdate = getAutoUpdateEnabled();
|
|
336
339
|
const maxBatch = getMaxBatchSize();
|
|
337
340
|
|
|
338
341
|
// Validate API key
|
|
@@ -358,6 +361,9 @@ export async function showStatus(options = {}) {
|
|
|
358
361
|
},
|
|
359
362
|
settings: {
|
|
360
363
|
autoCommit,
|
|
364
|
+
autoMerge,
|
|
365
|
+
autoComplete,
|
|
366
|
+
autoUpdate,
|
|
361
367
|
maxBatchSize: maxBatch
|
|
362
368
|
},
|
|
363
369
|
registeredProjects: registry.projectCount()
|
|
@@ -401,6 +407,9 @@ export async function showStatus(options = {}) {
|
|
|
401
407
|
|
|
402
408
|
// Settings
|
|
403
409
|
console.log(`${bold('Auto-commit:')} ${autoCommit ? 'Enabled' : 'Disabled'}`);
|
|
410
|
+
console.log(`${bold('Auto-merge:')} ${autoMerge ? 'Enabled' : 'Disabled'}`);
|
|
411
|
+
console.log(`${bold('Auto-complete:')} ${autoComplete ? 'Enabled' : 'Disabled'}`);
|
|
412
|
+
console.log(`${bold('Auto-update:')} ${autoUpdate ? 'Enabled' : 'Disabled'}`);
|
|
404
413
|
console.log(`${bold('Max batch size:')} ${maxBatch}`);
|
|
405
414
|
console.log(`${bold('Registered projects:')} ${status.registeredProjects}`);
|
|
406
415
|
}
|