create-claude-workspace 2.3.15 → 2.3.17

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.
@@ -193,8 +193,55 @@ export async function runIteration(deps) {
193
193
  if (queued > 0) {
194
194
  logger.info(`Starting ${tasksToRun.length} tasks, ${queued} queued (waiting for workers)`);
195
195
  }
196
- // Process recovered pipelines first (from recovery phase)
196
+ // ─── Phase A: Check active PR watches (non-blocking) ───
197
197
  let workDone = false;
198
+ const prWatchIds = Object.keys(state.pipelines).filter(id => state.pipelines[id].step === 'pr-watch');
199
+ for (const taskId of prWatchIds) {
200
+ const pipeline = state.pipelines[taskId];
201
+ const result = await checkPRWatch(taskId, pipeline, projectDir, agents, deps);
202
+ if (result === 'merged') {
203
+ let branch;
204
+ try {
205
+ branch = getCurrentBranch(pipeline.worktreePath);
206
+ }
207
+ catch {
208
+ branch = taskId;
209
+ }
210
+ logger.info(`[${taskId}] PR merged via platform`);
211
+ appendEvent(projectDir, createEvent('pr_merged', { taskId }));
212
+ syncMain(projectDir);
213
+ cleanupWorktree(projectDir, pipeline.worktreePath, branch);
214
+ deleteBranchRemote(projectDir, branch);
215
+ pipeline.step = 'done';
216
+ clearSession(state, taskId);
217
+ state.completedTasks.push(taskId);
218
+ delete state.pipelines[taskId];
219
+ if (state.taskMode === 'platform') {
220
+ const ciPlat = detectCIPlatform(projectDir);
221
+ if (ciPlat !== 'none') {
222
+ const issueNum = extractIssueNumber(taskId);
223
+ if (issueNum)
224
+ updateIssueStatus(projectDir, ciPlat, issueNum, 'done');
225
+ }
226
+ }
227
+ appendEvent(projectDir, createEvent('task_completed', { taskId }));
228
+ workDone = true;
229
+ }
230
+ else if (result === 'failed') {
231
+ logger.error(`[${taskId}] PR failed — skipping task`);
232
+ pipeline.step = 'failed';
233
+ state.skippedTasks.push(taskId);
234
+ delete state.pipelines[taskId];
235
+ workDone = true;
236
+ }
237
+ else if (result === 'rework') {
238
+ // CI failed or comments — agent was spawned to fix, push done, back to watching
239
+ workDone = true;
240
+ }
241
+ // result === 'waiting' → still polling, do nothing
242
+ writeState(projectDir, state);
243
+ }
244
+ // ─── Phase B: Process recovered pipelines (from recovery phase) ───
198
245
  const recoveredIds = Object.keys(state.pipelines).filter(id => {
199
246
  const p = state.pipelines[id];
200
247
  return p.workerId === -1 && p.step !== 'done' && p.step !== 'failed';
@@ -227,7 +274,11 @@ export async function runIteration(deps) {
227
274
  try {
228
275
  const success = await runTaskPipeline(task, slot.id, agents, deps);
229
276
  workDone = true;
230
- if (success) {
277
+ // If pipeline is in pr-watch, it's still active — don't mark done yet
278
+ if (state.pipelines[task.id]?.step === 'pr-watch') {
279
+ // PR watch is non-blocking, handled in Phase A next iteration
280
+ }
281
+ else if (success) {
231
282
  task.status = 'done';
232
283
  state.completedTasks.push(task.id);
233
284
  if (state.taskMode === 'platform') {
@@ -265,7 +316,11 @@ export async function runIteration(deps) {
265
316
  try {
266
317
  const success = await runTaskPipeline(task, slot.id, agents, deps);
267
318
  workDone = true;
268
- if (success) {
319
+ // If pipeline is in pr-watch, it's still active — don't mark done yet
320
+ if (state.pipelines[task.id]?.step === 'pr-watch') {
321
+ // PR watch is non-blocking, handled in Phase A next iteration
322
+ }
323
+ else if (success) {
269
324
  task.status = 'done';
270
325
  state.completedTasks.push(task.id);
271
326
  if (state.taskMode === 'platform') {
@@ -541,24 +596,13 @@ async function runTaskPipeline(task, workerId, agents, deps) {
541
596
  pipeline.prState = { prNumber: prInfo.number, url: prInfo.url, issueNumber: issueNum };
542
597
  logger.info(`[${task.id}] PR created: ${prInfo.url}`);
543
598
  appendEvent(projectDir, createEvent('pr_created', { taskId: task.id, detail: prInfo.url }));
544
- // STEP 7: Poll PR statusCI, comments, mergeable
599
+ // STEP 7: PR createdhand off to non-blocking PR watch
600
+ // The main loop checks pr-watch pipelines each iteration
545
601
  pipeline.step = 'pr-watch';
546
602
  appendEvent(projectDir, createEvent('step_changed', { taskId: task.id, step: 'pr-watch' }));
547
- const merged = await pollAndMergePR(task, pipeline, slug, ciPlatform, projectDir, worktreePath, agents, deps);
548
- if (merged) {
549
- logger.info(`[${task.id}] PR merged via platform`);
550
- appendEvent(projectDir, createEvent('pr_merged', { taskId: task.id }));
551
- // Sync main to get the merge commit
552
- syncMain(projectDir);
553
- cleanupWorktree(projectDir, worktreePath, slug);
554
- deleteBranchRemote(projectDir, slug);
555
- pipeline.step = 'done';
556
- clearSession(state, task.id);
557
- delete state.pipelines[task.id];
558
- return true;
559
- }
560
- // Fallback to local merge
561
- logger.warn(`[${task.id}] Platform merge failed — falling back to local merge`);
603
+ logger.info(`[${task.id}] PR watching releasing worker for other tasks`);
604
+ writeState(projectDir, state);
605
+ return true; // Task not failed, PR watch continues in main loop
562
606
  }
563
607
  catch (err) {
564
608
  logger.warn(`[${task.id}] PR creation failed: ${err.message} — falling back to local merge`);
@@ -902,13 +946,114 @@ function loadTasksJson(path) {
902
946
  // ─── PR polling and merge ───
903
947
  const PR_POLL_INTERVAL = 15_000;
904
948
  const PR_MAX_POLL_TIME = 30 * 60_000;
949
+ async function checkPRWatch(taskId, pipeline, projectDir, agents, deps) {
950
+ const { pool, logger, state, onMessage, onSpawnStart, onSpawnEnd } = deps;
951
+ const ciPlatform = detectCIPlatform(projectDir);
952
+ if (ciPlatform === 'none' || !pipeline.prState)
953
+ return 'failed';
954
+ const branch = getCurrentBranch(pipeline.worktreePath);
955
+ try {
956
+ const prStatus = getPRStatus(projectDir, ciPlatform, branch);
957
+ if (prStatus.status === 'merged')
958
+ return 'merged';
959
+ if (prStatus.status === 'closed')
960
+ return 'failed';
961
+ // Ready to merge
962
+ if (prStatus.mergeable) {
963
+ logger.info(`[${taskId}] PR mergeable — merging`);
964
+ const merged = mergePR(projectDir, ciPlatform, prStatus.number);
965
+ return merged ? 'merged' : 'failed';
966
+ }
967
+ // CI pending — keep waiting
968
+ if (prStatus.ciStatus === 'pending' || prStatus.ciStatus === 'not-found') {
969
+ logger.info(`[${taskId}] CI: ${prStatus.ciStatus} — waiting`);
970
+ return 'waiting';
971
+ }
972
+ // CI passed but not mergeable — waiting for approval
973
+ if (prStatus.ciStatus === 'passed' && !prStatus.mergeable) {
974
+ logger.info(`[${taskId}] CI passed, not mergeable — waiting`);
975
+ return 'waiting';
976
+ }
977
+ // CI failed — delegate fix to implementing agent
978
+ if (prStatus.ciStatus === 'failed') {
979
+ if (pipeline.ciFixes >= MAX_CI_FIXES) {
980
+ logger.error(`[${taskId}] CI fix limit (${MAX_CI_FIXES}) reached`);
981
+ return 'failed';
982
+ }
983
+ pipeline.ciFixes++;
984
+ logger.warn(`[${taskId}] CI failed — delegating to agent (${pipeline.ciFixes}/${MAX_CI_FIXES})`);
985
+ const slot = pool.idleSlot();
986
+ if (!slot) {
987
+ logger.info(`[${taskId}] No idle worker for CI fix — will retry next iteration`);
988
+ pipeline.ciFixes--; // Don't count this attempt
989
+ return 'waiting';
990
+ }
991
+ const logs = fetchFailureLogs(branch, ciPlatform === 'github' ? 'github' : 'gitlab', projectDir);
992
+ // Create a minimal task for agent spawning
993
+ const task = {
994
+ id: taskId, title: `Fix CI for ${taskId}`, phase: state.currentPhase,
995
+ type: 'fullstack', complexity: 'S', dependsOn: [], issueMarker: taskId,
996
+ kitUpgrade: false, lineNumber: 0, status: 'in-progress', changelog: 'fixed',
997
+ };
998
+ const fixResult = await spawnAgent(pool, slot.id, {
999
+ agent: pipeline.assignedAgent ?? undefined,
1000
+ cwd: pipeline.worktreePath,
1001
+ prompt: buildCIFixPrompt({ task, worktreePath: pipeline.worktreePath, projectDir }, logs ?? 'No CI logs available'),
1002
+ model: getAgentModel(pipeline.assignedAgent, agents, task),
1003
+ }, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd);
1004
+ if (fixResult.success) {
1005
+ const sha = commitInWorktree(pipeline.worktreePath, `fix: CI failure for ${taskId}`);
1006
+ if (sha) {
1007
+ forcePushWorktree(pipeline.worktreePath);
1008
+ logger.info(`[${taskId}] Fix pushed — will check CI next iteration`);
1009
+ return 'rework';
1010
+ }
1011
+ }
1012
+ return 'waiting'; // Agent didn't produce fix, retry next iteration
1013
+ }
1014
+ // Unresolved PR comments
1015
+ const comments = getPRComments(projectDir, ciPlatform, prStatus.number);
1016
+ const unresolved = comments.filter(c => !c.resolved);
1017
+ if (unresolved.length > 0) {
1018
+ const slot = pool.idleSlot();
1019
+ if (!slot)
1020
+ return 'waiting';
1021
+ logger.info(`[${taskId}] ${unresolved.length} PR comments — delegating to agent`);
1022
+ const commentText = unresolved
1023
+ .map(c => `${c.path ?? 'general'}${c.line ? `:${c.line}` : ''} — ${c.author}: ${c.body}`)
1024
+ .join('\n\n');
1025
+ const task = {
1026
+ id: taskId, title: `Address PR comments for ${taskId}`, phase: state.currentPhase,
1027
+ type: 'fullstack', complexity: 'S', dependsOn: [], issueMarker: taskId,
1028
+ kitUpgrade: false, lineNumber: 0, status: 'in-progress', changelog: 'fixed',
1029
+ };
1030
+ const result = await spawnAgent(pool, slot.id, {
1031
+ agent: pipeline.assignedAgent ?? undefined,
1032
+ cwd: pipeline.worktreePath,
1033
+ prompt: buildPRCommentPrompt({ task, worktreePath: pipeline.worktreePath, projectDir }, commentText),
1034
+ model: getAgentModel(pipeline.assignedAgent, agents, task),
1035
+ }, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd);
1036
+ if (result.success) {
1037
+ const sha = commitInWorktree(pipeline.worktreePath, 'fix: address PR comments');
1038
+ if (sha) {
1039
+ forcePushWorktree(pipeline.worktreePath);
1040
+ appendEvent(projectDir, createEvent('pr_comment_resolved', { taskId, detail: `${unresolved.length} comments` }));
1041
+ return 'rework';
1042
+ }
1043
+ }
1044
+ return 'waiting';
1045
+ }
1046
+ return 'waiting';
1047
+ }
1048
+ catch (err) {
1049
+ logger.error(`[${taskId}] PR watch error: ${err.message?.split('\n')[0]}`);
1050
+ return 'waiting'; // Don't fail on transient errors
1051
+ }
1052
+ }
1053
+ // ─── Legacy blocking PR poll (kept for local merge fallback) ───
905
1054
  async function pollAndMergePR(task, pipeline, branch, platform, projectDir, worktreePath, agents, deps) {
906
1055
  const { pool, logger, state, onMessage, onSpawnStart, onSpawnEnd } = deps;
907
1056
  const startTime = Date.now();
908
- let lastSeenCIStatus = 'none'; // Track CI transitions to detect stale status
909
- // Initial wait — give CI time to pick up the new push
910
- logger.info(`[${task.id}] Waiting for CI pipeline to start...`);
911
- await sleep(PR_POLL_INTERVAL);
912
1057
  while (Date.now() - startTime < PR_MAX_POLL_TIME) {
913
1058
  const prStatus = getPRStatus(projectDir, platform, branch);
914
1059
  if (prStatus.status === 'merged')
@@ -918,37 +1063,22 @@ async function pollAndMergePR(task, pipeline, branch, platform, projectDir, work
918
1063
  // Ready to merge — CI passed and mergeable
919
1064
  if (prStatus.mergeable) {
920
1065
  logger.info(`[${task.id}] PR mergeable — merging`);
921
- const merged = mergePR(projectDir, platform, prStatus.number);
922
- return merged;
1066
+ return mergePR(projectDir, platform, prStatus.number);
923
1067
  }
924
- // CI still running or not started — wait
1068
+ // CI still running or not started — poll
925
1069
  if (prStatus.ciStatus === 'pending' || prStatus.ciStatus === 'not-found') {
926
- if (prStatus.ciStatus !== lastSeenCIStatus) {
927
- logger.info(`[${task.id}] CI: ${prStatus.ciStatus} — waiting...`);
928
- lastSeenCIStatus = prStatus.ciStatus;
929
- }
1070
+ logger.info(`[${task.id}] CI: ${prStatus.ciStatus} polling...`);
930
1071
  await sleep(PR_POLL_INTERVAL);
931
1072
  continue;
932
1073
  }
933
- // CI passed but not mergeable (needs approval, merge checks, etc.)
1074
+ // CI passed but not mergeable (needs approval, merge checks, etc.) — poll
934
1075
  if (prStatus.ciStatus === 'passed' && !prStatus.mergeable) {
935
- if (lastSeenCIStatus !== 'passed-not-mergeable') {
936
- logger.info(`[${task.id}] CI passed but not mergeable — waiting for approval or checks`);
937
- lastSeenCIStatus = 'passed-not-mergeable';
938
- }
1076
+ logger.info(`[${task.id}] CI passed, not yet mergeable — polling...`);
939
1077
  await sleep(PR_POLL_INTERVAL);
940
1078
  continue;
941
1079
  }
942
1080
  // CI failed — delegate to implementing agent to fix
943
1081
  if (prStatus.ciStatus === 'failed') {
944
- // Avoid re-fixing if we just pushed and the old pipeline result is stale
945
- if (lastSeenCIStatus === 'fix-pushed') {
946
- // We just pushed a fix — wait for new pipeline to start
947
- logger.info(`[${task.id}] Waiting for new CI pipeline after fix push...`);
948
- lastSeenCIStatus = 'pending-after-fix';
949
- await sleep(PR_POLL_INTERVAL * 2); // Extra wait for new pipeline
950
- continue;
951
- }
952
1082
  if (pipeline.ciFixes >= MAX_CI_FIXES) {
953
1083
  logger.error(`[${task.id}] CI fix limit (${MAX_CI_FIXES}) reached — giving up`);
954
1084
  return false;
@@ -966,13 +1096,13 @@ async function pollAndMergePR(task, pipeline, branch, platform, projectDir, work
966
1096
  const sha = commitInWorktree(worktreePath, `fix: CI failure for ${task.title}`);
967
1097
  if (sha) {
968
1098
  forcePushWorktree(worktreePath);
969
- lastSeenCIStatus = 'fix-pushed';
970
- logger.info(`[${task.id}] Fix pushed waiting for new CI pipeline`);
971
- await sleep(PR_POLL_INTERVAL * 2); // Wait for new pipeline to start
1099
+ logger.info(`[${task.id}] Fix pushed — polling for new CI pipeline`);
1100
+ // Poll until CI status changes from 'failed' (= new pipeline picked up)
1101
+ await waitForCIRefresh(task.id, projectDir, platform, branch, logger);
972
1102
  continue;
973
1103
  }
974
1104
  }
975
- // Fix didn't produce changes or failed wait and retry
1105
+ // Fix didn't produce changes — poll and let next iteration re-evaluate
976
1106
  await sleep(PR_POLL_INTERVAL);
977
1107
  continue;
978
1108
  }
@@ -994,8 +1124,9 @@ async function pollAndMergePR(task, pipeline, branch, platform, projectDir, work
994
1124
  const sha = commitInWorktree(worktreePath, 'fix: address PR review comments');
995
1125
  if (sha) {
996
1126
  forcePushWorktree(worktreePath);
997
- lastSeenCIStatus = 'fix-pushed';
998
1127
  appendEvent(projectDir, createEvent('pr_comment_resolved', { taskId: task.id, detail: `${unresolved.length} comments` }));
1128
+ await waitForCIRefresh(task.id, projectDir, platform, branch, logger);
1129
+ continue;
999
1130
  }
1000
1131
  }
1001
1132
  await sleep(PR_POLL_INTERVAL);
@@ -1006,6 +1137,24 @@ async function pollAndMergePR(task, pipeline, branch, platform, projectDir, work
1006
1137
  logger.warn(`[${task.id}] PR poll timeout (${PR_MAX_POLL_TIME / 60_000}min)`);
1007
1138
  return false;
1008
1139
  }
1140
+ /** Poll until CI status is no longer the previous 'failed' (= new pipeline started) */
1141
+ async function waitForCIRefresh(taskId, projectDir, platform, branch, logger) {
1142
+ const maxWait = 5 * 60_000; // 5 min max to wait for new pipeline
1143
+ const start = Date.now();
1144
+ while (Date.now() - start < maxWait) {
1145
+ await sleep(PR_POLL_INTERVAL);
1146
+ try {
1147
+ const status = getPRStatus(projectDir, platform, branch);
1148
+ if (status.ciStatus !== 'failed') {
1149
+ logger.info(`[${taskId}] New CI pipeline detected: ${status.ciStatus}`);
1150
+ return;
1151
+ }
1152
+ logger.info(`[${taskId}] CI still shows previous failure — polling...`);
1153
+ }
1154
+ catch { /* ignore */ }
1155
+ }
1156
+ logger.warn(`[${taskId}] Timed out waiting for new CI pipeline`);
1157
+ }
1009
1158
  function buildPRBody(task, pipeline) {
1010
1159
  const lines = [
1011
1160
  `## Summary`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.15",
3
+ "version": "2.3.17",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",