create-claude-workspace 2.2.0 → 2.2.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.
@@ -26,12 +26,15 @@ export function createPR(opts) {
26
26
  comments: [],
27
27
  };
28
28
  }
29
- // GitLab
30
- const output = cli(`glab mr create --title "${escapedTitle}" --description "${escapedBody}" --target-branch "${opts.baseBranch}" --source-branch "${opts.branch}" --no-editor --json iid,web_url`, opts.cwd, WRITE_TIMEOUT);
31
- const mr = JSON.parse(output);
29
+ // GitLab — glab mr create doesn't support --json, parse URL from output
30
+ const output = cli(`glab mr create --title "${escapedTitle}" --description "${escapedBody}" --target-branch "${opts.baseBranch}" --source-branch "${opts.branch}" --no-editor`, opts.cwd, WRITE_TIMEOUT);
31
+ // glab outputs the MR URL, e.g. "https://gitlab.com/group/repo/-/merge_requests/7"
32
+ const urlMatch = output.match(/https?:\/\/\S+merge_requests\/(\d+)/);
33
+ const mrIid = urlMatch ? parseInt(urlMatch[1], 10) : 0;
34
+ const mrUrl = urlMatch ? urlMatch[0] : output.trim();
32
35
  return {
33
- number: mr.iid,
34
- url: mr.web_url,
36
+ number: mrIid,
37
+ url: mrUrl,
35
38
  status: 'open',
36
39
  ciStatus: 'pending',
37
40
  approvals: 0,
@@ -83,7 +86,7 @@ function parseGitHubComments(comments) {
83
86
  }));
84
87
  }
85
88
  function getGitLabMRStatus(cwd, branch) {
86
- const output = cli(`glab mr view "${branch}" --json iid,web_url,state,merge_status,head_pipeline,user_notes_count`, cwd);
89
+ const output = cli(`glab mr view "${branch}" --output json`, cwd);
87
90
  const mr = JSON.parse(output);
88
91
  const ciStatus = resolveGitLabCIStatus(mr.head_pipeline);
89
92
  const mergeable = mr.merge_status === 'can_be_merged' && ciStatus === 'passed';
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",