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.
- package/dist/scheduler/loop.mjs +197 -48
- package/package.json +1 -1
package/dist/scheduler/loop.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
599
|
+
// STEP 7: PR created — hand 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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
922
|
-
return merged;
|
|
1066
|
+
return mergePR(projectDir, platform, prStatus.number);
|
|
923
1067
|
}
|
|
924
|
-
// CI still running or not started —
|
|
1068
|
+
// CI still running or not started — poll
|
|
925
1069
|
if (prStatus.ciStatus === 'pending' || prStatus.ciStatus === 'not-found') {
|
|
926
|
-
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
await
|
|
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
|
|
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`,
|