create-claude-workspace 2.3.16 → 2.3.18

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.
@@ -23,18 +23,20 @@ export function createPR(opts) {
23
23
  ? `${opts.body}\n\nCloses #${opts.issueNumber}`
24
24
  : opts.body;
25
25
  if (opts.platform === 'github') {
26
- const output = run('gh', [
26
+ // gh pr create outputs the PR URL on success
27
+ const url = run('gh', [
27
28
  'pr', 'create',
28
29
  '--title', opts.title,
29
30
  '--body', body,
30
31
  '--base', opts.baseBranch,
31
32
  '--head', opts.branch,
32
- '--json', 'number,url',
33
33
  ], opts.cwd, WRITE_TIMEOUT);
34
- const pr = JSON.parse(output);
34
+ // Extract PR number from URL: https://github.com/owner/repo/pull/123
35
+ const prNumMatch = url.match(/\/pull\/(\d+)/);
36
+ const prNumber = prNumMatch ? parseInt(prNumMatch[1], 10) : 0;
35
37
  return {
36
- number: pr.number,
37
- url: pr.url,
38
+ number: prNumber,
39
+ url: url.trim(),
38
40
  status: 'open',
39
41
  ciStatus: 'pending',
40
42
  approvals: 0,
@@ -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,6 +946,111 @@ 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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.16",
3
+ "version": "2.3.18",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",