create-claude-workspace 2.3.16 → 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 +168 -19
- 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,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();
|