create-claude-workspace 2.3.10 → 2.3.11

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.
@@ -55,13 +55,16 @@ export function listOrphanedWorktrees(projectDir, knownWorktrees) {
55
55
  return actual.filter(p => !knownSet.has(normalizePath(resolve(p))));
56
56
  }
57
57
  // ─── Commit operations ───
58
- export function commitInWorktree(worktreePath, message) {
58
+ export function commitInWorktree(worktreePath, message, skipHooks = false) {
59
59
  git(['add', '-A'], worktreePath);
60
60
  // Check if there's anything to commit
61
61
  const status = git(['status', '--porcelain'], worktreePath);
62
62
  if (!status)
63
63
  return '';
64
- git(['commit', '-m', message], worktreePath);
64
+ const args = ['commit', '-m', message];
65
+ if (skipHooks)
66
+ args.push('--no-verify');
67
+ git(args, worktreePath);
65
68
  return git(['rev-parse', 'HEAD'], worktreePath);
66
69
  }
67
70
  export function getChangedFiles(worktreePath) {
@@ -1,13 +1,14 @@
1
1
  // ─── Main orchestration loop: pipeline state machine ───
2
2
  // Picks tasks, assigns to workers, advances through plan→implement→test→review→commit→merge.
3
3
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
4
+ import { execFileSync } from 'node:child_process';
4
5
  import { resolve } from 'node:path';
5
6
  import { parseTodoMd, updateTaskCheckbox } from './tasks/parser.mjs';
6
7
  import { fetchOpenIssues, issueToTask, updateIssueStatus } from './tasks/issue-source.mjs';
7
8
  import { buildGraph, getParallelBatches, isPhaseComplete, getNextPhase, isProjectComplete } from './tasks/queue.mjs';
8
9
  import { writeState, appendEvent, createEvent, rotateLog } from './state/state.mjs';
9
10
  import { recordSession, getSession, clearSession } from './state/session.mjs';
10
- import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, cleanMainForMerge, popStash, getMainBranch, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, abortMerge, deleteBranchRemote, } from './git/manager.mjs';
11
+ import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, cleanMainForMerge, popStash, getMainBranch, listWorktrees, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, abortMerge, deleteBranchRemote, } from './git/manager.mjs';
11
12
  import { createPR, getPRStatus, getPRComments, mergePR } from './git/pr-manager.mjs';
12
13
  import { scanAgents } from './agents/health-checker.mjs';
13
14
  import { detectCIPlatform, fetchFailureLogs } from './git/ci-watcher.mjs';
@@ -63,7 +64,7 @@ export async function runIteration(deps) {
63
64
  if (hasUncommittedChanges(projectDir)) {
64
65
  logger.warn('Uncommitted changes on main — auto-committing before starting work');
65
66
  try {
66
- commitInWorktree(projectDir, 'chore: auto-commit uncommitted changes before scheduler start');
67
+ commitInWorktree(projectDir, 'chore: auto-commit uncommitted changes before scheduler start', true);
67
68
  logger.info('Uncommitted changes committed on main');
68
69
  }
69
70
  catch (err) {
@@ -593,166 +594,135 @@ function closeRecoveredIssue(projectDir, branch, logger) {
593
594
  async function recoverOrphanedWorktrees(projectDir, state, logger, deps) {
594
595
  const knownPaths = Object.values(state.pipelines).map(p => p.worktreePath);
595
596
  const orphans = listOrphanedWorktrees(projectDir, knownPaths);
596
- if (orphans.length === 0)
597
- return;
598
- logger.info(`Found ${orphans.length} orphaned worktree(s) — attempting recovery`);
597
+ const ciPlatform = detectCIPlatform(projectDir);
598
+ if (orphans.length === 0) {
599
+ logger.info('[recovery] No orphaned worktrees found');
600
+ }
601
+ else {
602
+ logger.info(`[recovery] Found ${orphans.length} orphaned worktree(s)`);
603
+ }
604
+ // Phase 1: Handle orphaned worktrees
599
605
  for (const worktreePath of orphans) {
600
606
  try {
601
607
  const branch = getCurrentBranch(worktreePath);
608
+ logger.info(`[recovery] Processing worktree: ${branch}`);
602
609
  const alreadyMerged = isBranchMerged(projectDir, branch);
603
610
  if (alreadyMerged) {
604
- // Already merged — just clean up
605
- logger.info(`[recovery] ${branch} already merged — cleaning up worktree`);
611
+ logger.info(`[recovery] ${branch} already merged — cleaning up`);
606
612
  cleanupWorktree(projectDir, worktreePath, branch);
607
613
  deleteBranchRemote(projectDir, branch);
608
614
  closeRecoveredIssue(projectDir, branch, logger);
609
615
  continue;
610
616
  }
611
- // Check if worktree has commits ahead of main
612
617
  const hasChanges = hasUncommittedChanges(worktreePath);
613
618
  const changedFromMain = getChangedFiles(worktreePath);
614
619
  const hasCommits = changedFromMain.length > 0;
620
+ logger.info(`[recovery] ${branch}: uncommitted=${hasChanges}, commits ahead=${hasCommits} (${changedFromMain.length} files)`);
615
621
  if (!hasCommits && !hasChanges) {
616
- // No work done — clean up empty worktree
617
622
  logger.info(`[recovery] ${branch} has no changes — cleaning up`);
618
623
  cleanupWorktree(projectDir, worktreePath, branch);
624
+ deleteBranchRemote(projectDir, branch);
619
625
  continue;
620
626
  }
621
627
  if (hasChanges) {
622
- // Uncommitted changes — commit them first
623
628
  logger.info(`[recovery] ${branch} has uncommitted changes — committing`);
624
- commitInWorktree(worktreePath, `wip: auto-commit from recovery`);
629
+ commitInWorktree(worktreePath, 'wip: auto-commit from recovery', true);
630
+ }
631
+ // Delegate to AI agent: rebase onto main, fix any issues, then commit
632
+ logger.info(`[recovery] Spawning AI agent to finalize ${branch}`);
633
+ const slot = deps.pool.idleSlot();
634
+ if (!slot) {
635
+ logger.warn(`[recovery] No idle worker — skipping ${branch} for now`);
636
+ continue;
625
637
  }
626
- // Try to merge
627
- logger.info(`[recovery] ${branch} has unmerged commits — attempting merge`);
628
- // Clean dirty main before merge
629
- // Clean main: abort any in-progress merge/rebase, stash uncommitted changes
638
+ deps.onSpawnStart?.(`recovery-${branch}`);
639
+ try {
640
+ await deps.pool.spawn(slot.id, {
641
+ cwd: worktreePath,
642
+ prompt: [
643
+ `You are in a git worktree for branch "${branch}".`,
644
+ `This branch was interrupted and needs to be finalized for merge into main.`,
645
+ ``,
646
+ `Do the following:`,
647
+ `1. Run: git fetch origin main && git rebase origin/main`,
648
+ `2. If rebase has conflicts, resolve them sensibly (prefer feature branch intent)`,
649
+ `3. After rebase, run the project's build/lint to check for errors:`,
650
+ ` - Look at package.json for available scripts (build, lint, test)`,
651
+ ` - Fix any build or lint errors in the code`,
652
+ `4. Commit fixes: git add -A && git commit --no-verify -m "fix: resolve issues for merge"`,
653
+ `5. Do NOT merge into main — just make the branch clean and ready`,
654
+ ``,
655
+ `Changed files in this branch: ${changedFromMain.join(', ')}`,
656
+ ].join('\n'),
657
+ model: 'claude-sonnet-4-6',
658
+ onMessage: deps.onMessage,
659
+ });
660
+ }
661
+ finally {
662
+ deps.onSpawnEnd?.();
663
+ }
664
+ // Now try to merge
665
+ logger.info(`[recovery] Agent done — attempting merge of ${branch}`);
630
666
  const stashed = cleanMainForMerge(projectDir);
631
667
  syncMain(projectDir);
632
- // Try PR flow first if remote exists
633
- const ciPlatform = detectCIPlatform(projectDir);
668
+ // Try platform merge (PR/MR) first
669
+ let merged = false;
634
670
  if (ciPlatform !== 'none') {
635
- const pushed = pushWorktree(worktreePath);
671
+ const pushed = pushWorktree(worktreePath) || forcePushWorktree(worktreePath);
636
672
  if (pushed) {
637
673
  try {
638
674
  const mainBranch = getMainBranch(projectDir);
675
+ const issueNum = extractIssueFromBranch(branch);
639
676
  const prInfo = createPR({
640
677
  cwd: projectDir,
641
678
  platform: ciPlatform,
642
679
  branch,
643
680
  baseBranch: mainBranch,
644
- title: `feat: ${branch} (recovered)`,
645
- body: `Auto-recovered from orphaned worktree.\n\nThis PR was created by the scheduler to complete a previously interrupted task.`,
681
+ title: `feat: ${branch}`,
682
+ body: 'Recovered and finalized by scheduler.',
683
+ issueNumber: issueNum ?? undefined,
646
684
  });
647
- logger.info(`[recovery] PR created for ${branch}: ${prInfo.url}`);
648
- // Try to merge immediately if possible
649
- const status = getPRStatus(projectDir, ciPlatform, branch);
650
- if (status.mergeable) {
651
- mergePR(projectDir, ciPlatform, status.number);
652
- logger.info(`[recovery] ${branch} merged via platform`);
653
- syncMain(projectDir);
654
- cleanupWorktree(projectDir, worktreePath, branch);
655
- deleteBranchRemote(projectDir, branch);
656
- closeRecoveredIssue(projectDir, branch, logger);
657
- if (stashed)
658
- popStash(projectDir);
659
- continue;
685
+ logger.info(`[recovery] PR: ${prInfo.url}`);
686
+ if (prInfo.status === 'merged') {
687
+ logger.info(`[recovery] ${branch} already merged via platform`);
688
+ merged = true;
689
+ }
690
+ else {
691
+ const prStatus = getPRStatus(projectDir, ciPlatform, branch);
692
+ if (prStatus.mergeable) {
693
+ merged = mergePR(projectDir, ciPlatform, prStatus.number);
694
+ if (merged)
695
+ logger.info(`[recovery] ${branch} merged via platform`);
696
+ }
697
+ else {
698
+ logger.info(`[recovery] PR for ${branch} not yet mergeable (CI: ${prStatus.ciStatus}) — leaving open`);
699
+ }
660
700
  }
661
- // PR created but not yet mergeable — leave it for next iteration
662
- logger.info(`[recovery] PR for ${branch} not yet mergeable — will retry`);
663
- if (stashed)
664
- popStash(projectDir);
665
- continue;
666
701
  }
667
702
  catch (err) {
668
- logger.warn(`[recovery] PR creation failed for ${branch}: ${err.message} — trying local merge`);
703
+ logger.warn(`[recovery] PR flow failed for ${branch}: ${err.message?.split('\n')[0]}`);
669
704
  }
670
705
  }
671
706
  }
672
707
  // Local merge fallback
673
- const mergeResult = mergeToMain(projectDir, branch);
674
- if (mergeResult.success) {
675
- logger.info(`[recovery] ${branch} merged locally (${mergeResult.sha?.slice(0, 7)})`);
676
- appendEvent(projectDir, createEvent('merge_completed', { detail: `recovery: ${branch}` }));
677
- closeRecoveredIssue(projectDir, branch, logger);
678
- }
679
- else if (mergeResult.conflict) {
680
- // mergeToMain already aborted the merge internally
681
- // Delegate conflict resolution to orchestrator AI
682
- logger.info(`[recovery] ${branch} has merge conflicts — delegating to AI`);
683
- let resolved = false;
684
- try {
685
- const conflictDecision = await deps.orchestrator.handleMergeConflict(branch, mergeResult.conflictFiles ?? []);
686
- if (conflictDecision.action === 'resolve') {
687
- // Spawn agent to resolve conflicts in the worktree via rebase
688
- logger.info(`[recovery] Spawning agent to resolve conflicts in ${branch}`);
689
- const conflictPrompt = [
690
- `You are in a worktree for branch "${branch}".`,
691
- `Rebase this branch onto main and resolve all merge conflicts.`,
692
- ``,
693
- `Steps:`,
694
- `1. Run: git rebase main`,
695
- `2. For each conflict, read the conflicting files and resolve them sensibly`,
696
- `3. After resolving each file: git add <file>`,
697
- `4. Continue rebase: git rebase --continue`,
698
- `5. Repeat until rebase is complete`,
699
- ``,
700
- `Conflicting files: ${(mergeResult.conflictFiles ?? []).join(', ') || 'unknown'}`,
701
- ``,
702
- `Important: preserve the intent of both sides. When in doubt, prefer the feature branch changes.`,
703
- ].join('\n');
704
- const slot = deps.pool.idleSlot();
705
- if (slot) {
706
- await deps.pool.spawn(slot.id, {
707
- cwd: worktreePath,
708
- prompt: conflictPrompt,
709
- model: 'claude-sonnet-4-6',
710
- onMessage: deps.onMessage,
711
- });
712
- // Retry merge after agent resolved conflicts
713
- const retryResult = mergeToMain(projectDir, branch);
714
- if (retryResult.success) {
715
- logger.info(`[recovery] ${branch} merged after AI conflict resolution`);
716
- appendEvent(projectDir, createEvent('merge_completed', { detail: `recovery-ai-resolve: ${branch}` }));
717
- resolved = true;
718
- }
719
- }
720
- }
721
- else if (conflictDecision.action === 'rebase') {
722
- const rebaseResult = rebaseOnMain(worktreePath, projectDir);
723
- if (rebaseResult.success) {
724
- const retryResult = mergeToMain(projectDir, branch);
725
- if (retryResult.success) {
726
- logger.info(`[recovery] ${branch} merged after rebase`);
727
- resolved = true;
728
- }
729
- }
730
- }
731
- // action === 'skip' → resolved stays false
732
- }
733
- catch (err) {
734
- logger.warn(`[recovery] AI conflict resolution failed for ${branch}: ${err.message}`);
735
- abortMerge(projectDir);
708
+ if (!merged) {
709
+ const mergeResult = mergeToMain(projectDir, branch);
710
+ if (mergeResult.success) {
711
+ logger.info(`[recovery] ${branch} merged locally (${mergeResult.sha?.slice(0, 7)})`);
712
+ merged = true;
736
713
  }
737
- if (!resolved) {
738
- logger.error(`[recovery] ${branch} has unresolvable conflicts skipping`);
714
+ else {
715
+ logger.error(`[recovery] ${branch} merge failed: ${mergeResult.error ?? 'unknown'}`);
739
716
  abortMerge(projectDir);
740
- if (stashed)
741
- popStash(projectDir);
742
- continue;
743
717
  }
744
718
  }
745
- else {
746
- logger.error(`[recovery] ${branch} merge failed: ${mergeResult.error} — skipping`);
747
- abortMerge(projectDir);
748
- if (stashed)
749
- popStash(projectDir);
750
- continue;
719
+ if (merged) {
720
+ syncMain(projectDir);
721
+ cleanupWorktree(projectDir, worktreePath, branch);
722
+ deleteBranchRemote(projectDir, branch);
723
+ closeRecoveredIssue(projectDir, branch, logger);
724
+ appendEvent(projectDir, createEvent('merge_completed', { detail: `recovery: ${branch}` }));
751
725
  }
752
- // Post-merge cleanup: worktree + local branch + remote branch + close issue
753
- cleanupWorktree(projectDir, worktreePath, branch);
754
- deleteBranchRemote(projectDir, branch);
755
- closeRecoveredIssue(projectDir, branch, logger);
756
726
  if (stashed)
757
727
  popStash(projectDir);
758
728
  }
@@ -760,6 +730,69 @@ async function recoverOrphanedWorktrees(projectDir, state, logger, deps) {
760
730
  logger.error(`[recovery] Failed to recover ${worktreePath}: ${err.message}`);
761
731
  }
762
732
  }
733
+ // Phase 2: Check for open MRs/PRs without local worktrees
734
+ if (ciPlatform !== 'none') {
735
+ await recoverOpenMRs(projectDir, ciPlatform, state, logger);
736
+ }
737
+ }
738
+ /** Find open MRs/PRs that don't have local worktrees and try to merge them */
739
+ async function recoverOpenMRs(projectDir, platform, state, logger) {
740
+ try {
741
+ // Get all open MRs
742
+ const openMRs = getOpenMRs(projectDir, platform);
743
+ if (openMRs.length === 0)
744
+ return;
745
+ const existingBranches = new Set(listWorktrees(projectDir).map(wt => {
746
+ try {
747
+ return getCurrentBranch(wt);
748
+ }
749
+ catch {
750
+ return '';
751
+ }
752
+ }));
753
+ for (const mr of openMRs) {
754
+ if (existingBranches.has(mr.branch))
755
+ continue; // Handled in phase 1
756
+ logger.info(`[recovery] Found open MR !${mr.number} for ${mr.branch} (no local worktree)`);
757
+ const prStatus = getPRStatus(projectDir, platform, mr.branch);
758
+ if (prStatus.status === 'merged') {
759
+ logger.info(`[recovery] MR !${mr.number} already merged`);
760
+ closeRecoveredIssue(projectDir, mr.branch, logger);
761
+ continue;
762
+ }
763
+ if (prStatus.mergeable) {
764
+ const merged = mergePR(projectDir, platform, prStatus.number);
765
+ if (merged) {
766
+ logger.info(`[recovery] MR !${mr.number} merged via platform`);
767
+ syncMain(projectDir);
768
+ closeRecoveredIssue(projectDir, mr.branch, logger);
769
+ }
770
+ }
771
+ else {
772
+ logger.info(`[recovery] MR !${mr.number} not mergeable (CI: ${prStatus.ciStatus}) — leaving open`);
773
+ }
774
+ }
775
+ }
776
+ catch (err) {
777
+ logger.warn(`[recovery] MR scan failed: ${err.message?.split('\n')[0]}`);
778
+ }
779
+ }
780
+ function getOpenMRs(projectDir, platform) {
781
+ try {
782
+ if (platform === 'github') {
783
+ const output = execFileSync('gh', ['pr', 'list', '--json', 'number,headRefName', '--state', 'open'], { cwd: projectDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30_000 }).trim();
784
+ const prs = JSON.parse(output);
785
+ return prs.map(p => ({ number: p.number, branch: p.headRefName }));
786
+ }
787
+ else {
788
+ const output = execFileSync('glab', ['mr', 'list', '--output', 'json'], { cwd: projectDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30_000 }).trim();
789
+ const mrs = JSON.parse(output);
790
+ return mrs.map(m => ({ number: m.iid, branch: m.source_branch }));
791
+ }
792
+ }
793
+ catch {
794
+ return [];
795
+ }
763
796
  }
764
797
  function loadTasksJson(path) {
765
798
  const content = readFileSync(path, 'utf-8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.10",
3
+ "version": "2.3.11",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",