create-claude-workspace 2.2.0 → 2.2.1
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/dist/scheduler/loop.mjs +127 -1
- package/package.json +1 -1
package/dist/scheduler/loop.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fetchOpenIssues, issueToTask, updateIssueStatus } from './tasks/issue-s
|
|
|
7
7
|
import { buildGraph, getParallelBatches, isPhaseComplete, getNextPhase, isProjectComplete } from './tasks/queue.mjs';
|
|
8
8
|
import { writeState, appendEvent, createEvent, rotateLog } from './state/state.mjs';
|
|
9
9
|
import { recordSession, getSession, clearSession } from './state/session.mjs';
|
|
10
|
-
import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, evaluateDirtyMain, discardFiles, stashChanges, popStash, getMainBranch, } from './git/manager.mjs';
|
|
10
|
+
import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, evaluateDirtyMain, discardFiles, stashChanges, popStash, getMainBranch, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, } from './git/manager.mjs';
|
|
11
11
|
import { createPR, getPRStatus, getPRComments, mergePR } from './git/pr-manager.mjs';
|
|
12
12
|
import { scanAgents } from './agents/health-checker.mjs';
|
|
13
13
|
import { detectCIPlatform, fetchFailureLogs } from './git/ci-watcher.mjs';
|
|
@@ -58,6 +58,10 @@ export async function runIteration(deps) {
|
|
|
58
58
|
// Caller (scheduler.mts) handles pause state
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
// Recover orphaned worktrees from previous runs
|
|
62
|
+
if (state.iteration === 0) {
|
|
63
|
+
await recoverOrphanedWorktrees(projectDir, state, logger, deps);
|
|
64
|
+
}
|
|
61
65
|
// Load tasks (dual mode: platform issues or local TODO.md)
|
|
62
66
|
let tasks;
|
|
63
67
|
if (state.taskMode === 'platform') {
|
|
@@ -507,6 +511,128 @@ function extractSection(output, section) {
|
|
|
507
511
|
const match = output.match(pattern);
|
|
508
512
|
return match ? match[1].trim() : null;
|
|
509
513
|
}
|
|
514
|
+
// ─── Orphaned worktree recovery ───
|
|
515
|
+
async function recoverOrphanedWorktrees(projectDir, state, logger, deps) {
|
|
516
|
+
const knownPaths = Object.values(state.pipelines).map(p => p.worktreePath);
|
|
517
|
+
const orphans = listOrphanedWorktrees(projectDir, knownPaths);
|
|
518
|
+
if (orphans.length === 0)
|
|
519
|
+
return;
|
|
520
|
+
logger.info(`Found ${orphans.length} orphaned worktree(s) — attempting recovery`);
|
|
521
|
+
for (const worktreePath of orphans) {
|
|
522
|
+
try {
|
|
523
|
+
const branch = getCurrentBranch(worktreePath);
|
|
524
|
+
const alreadyMerged = isBranchMerged(projectDir, branch);
|
|
525
|
+
if (alreadyMerged) {
|
|
526
|
+
// Already merged — just clean up
|
|
527
|
+
logger.info(`[recovery] ${branch} already merged — cleaning up worktree`);
|
|
528
|
+
cleanupWorktree(projectDir, worktreePath, branch);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
// Check if worktree has commits ahead of main
|
|
532
|
+
const hasChanges = hasUncommittedChanges(worktreePath);
|
|
533
|
+
const changedFromMain = getChangedFiles(worktreePath);
|
|
534
|
+
const hasCommits = changedFromMain.length > 0;
|
|
535
|
+
if (!hasCommits && !hasChanges) {
|
|
536
|
+
// No work done — clean up empty worktree
|
|
537
|
+
logger.info(`[recovery] ${branch} has no changes — cleaning up`);
|
|
538
|
+
cleanupWorktree(projectDir, worktreePath, branch);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (hasChanges) {
|
|
542
|
+
// Uncommitted changes — commit them first
|
|
543
|
+
logger.info(`[recovery] ${branch} has uncommitted changes — committing`);
|
|
544
|
+
commitInWorktree(worktreePath, `wip: auto-commit from recovery`);
|
|
545
|
+
}
|
|
546
|
+
// Try to merge
|
|
547
|
+
logger.info(`[recovery] ${branch} has unmerged commits — attempting merge`);
|
|
548
|
+
// Clean dirty main before merge
|
|
549
|
+
const dirty = evaluateDirtyMain(projectDir);
|
|
550
|
+
if (dirty.trackingFiles.length > 0) {
|
|
551
|
+
discardFiles(projectDir, dirty.trackingFiles);
|
|
552
|
+
}
|
|
553
|
+
const stashed = dirty.userFiles.length > 0 ? stashChanges(projectDir) : false;
|
|
554
|
+
syncMain(projectDir);
|
|
555
|
+
// Try PR flow first if remote exists
|
|
556
|
+
const ciPlatform = detectCIPlatform(projectDir);
|
|
557
|
+
if (ciPlatform !== 'none') {
|
|
558
|
+
const pushed = pushWorktree(worktreePath);
|
|
559
|
+
if (pushed) {
|
|
560
|
+
try {
|
|
561
|
+
const mainBranch = getMainBranch(projectDir);
|
|
562
|
+
const prInfo = createPR({
|
|
563
|
+
cwd: projectDir,
|
|
564
|
+
platform: ciPlatform,
|
|
565
|
+
branch,
|
|
566
|
+
baseBranch: mainBranch,
|
|
567
|
+
title: `feat: ${branch} (recovered)`,
|
|
568
|
+
body: `Auto-recovered from orphaned worktree.\n\nThis PR was created by the scheduler to complete a previously interrupted task.`,
|
|
569
|
+
});
|
|
570
|
+
logger.info(`[recovery] PR created for ${branch}: ${prInfo.url}`);
|
|
571
|
+
// Try to merge immediately if possible
|
|
572
|
+
const status = getPRStatus(projectDir, ciPlatform, branch);
|
|
573
|
+
if (status.mergeable) {
|
|
574
|
+
mergePR(projectDir, ciPlatform, status.number);
|
|
575
|
+
logger.info(`[recovery] ${branch} merged via platform`);
|
|
576
|
+
syncMain(projectDir);
|
|
577
|
+
cleanupWorktree(projectDir, worktreePath, branch);
|
|
578
|
+
if (stashed)
|
|
579
|
+
popStash(projectDir);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
// PR created but not yet mergeable — leave it for next iteration
|
|
583
|
+
logger.info(`[recovery] PR for ${branch} not yet mergeable — will retry`);
|
|
584
|
+
if (stashed)
|
|
585
|
+
popStash(projectDir);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
logger.warn(`[recovery] PR creation failed for ${branch}: ${err.message} — trying local merge`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Local merge fallback
|
|
594
|
+
const mergeResult = mergeToMain(projectDir, branch);
|
|
595
|
+
if (mergeResult.success) {
|
|
596
|
+
logger.info(`[recovery] ${branch} merged locally (${mergeResult.sha?.slice(0, 7)})`);
|
|
597
|
+
appendEvent(projectDir, createEvent('merge_completed', { detail: `recovery: ${branch}` }));
|
|
598
|
+
}
|
|
599
|
+
else if (mergeResult.conflict) {
|
|
600
|
+
// Try rebase
|
|
601
|
+
const rebaseResult = rebaseOnMain(worktreePath, projectDir);
|
|
602
|
+
if (rebaseResult.success) {
|
|
603
|
+
const retryResult = mergeToMain(projectDir, branch);
|
|
604
|
+
if (retryResult.success) {
|
|
605
|
+
logger.info(`[recovery] ${branch} merged after rebase`);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
logger.error(`[recovery] ${branch} merge failed after rebase — skipping`);
|
|
609
|
+
if (stashed)
|
|
610
|
+
popStash(projectDir);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
logger.error(`[recovery] ${branch} has unresolvable conflicts — skipping`);
|
|
616
|
+
if (stashed)
|
|
617
|
+
popStash(projectDir);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
logger.error(`[recovery] ${branch} merge failed: ${mergeResult.error} — skipping`);
|
|
623
|
+
if (stashed)
|
|
624
|
+
popStash(projectDir);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
cleanupWorktree(projectDir, worktreePath, branch);
|
|
628
|
+
if (stashed)
|
|
629
|
+
popStash(projectDir);
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
logger.error(`[recovery] Failed to recover ${worktreePath}: ${err.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
510
636
|
function loadTasksJson(path) {
|
|
511
637
|
const content = readFileSync(path, 'utf-8');
|
|
512
638
|
const data = JSON.parse(content);
|