create-claude-workspace 2.3.34 → 2.3.36
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 +74 -59
- package/package.json +1 -1
package/dist/scheduler/loop.mjs
CHANGED
|
@@ -65,7 +65,8 @@ export async function runIteration(deps) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
// Pre-flight: commit uncommitted changes on main before any work
|
|
68
|
-
|
|
68
|
+
// Interactive mode: skip recovery — user controls what runs
|
|
69
|
+
if (!state._recoveryDone && state.taskMode !== 'interactive') {
|
|
69
70
|
if (hasUncommittedChanges(projectDir)) {
|
|
70
71
|
logger.warn('Uncommitted changes on main — auto-committing before starting work');
|
|
71
72
|
try {
|
|
@@ -77,9 +78,9 @@ export async function runIteration(deps) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
// Recover orphaned worktrees — once per scheduler run (first iteration only)
|
|
80
|
-
state._recoveryDone = true;
|
|
81
81
|
await recoverOrphanedWorktrees(projectDir, state, logger, deps);
|
|
82
82
|
}
|
|
83
|
+
state._recoveryDone = true;
|
|
83
84
|
// Load tasks (triple mode: interactive = inbox only, platform = issues, local = TODO.md)
|
|
84
85
|
let tasks;
|
|
85
86
|
if (state.taskMode === 'interactive') {
|
|
@@ -159,62 +160,11 @@ export async function runIteration(deps) {
|
|
|
159
160
|
task.status = 'skipped';
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
|
-
//
|
|
163
|
-
if (isProjectComplete(tasks)) {
|
|
164
|
-
logger.info('All tasks complete.');
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
// Scan agents
|
|
163
|
+
// Scan agents (needed by Phase A/B/C)
|
|
168
164
|
const agents = scanAgents(projectDir);
|
|
169
|
-
// Build dependency graph
|
|
170
|
-
const graph = buildGraph(tasks);
|
|
171
|
-
const cycle = graph.findCycle();
|
|
172
|
-
if (cycle) {
|
|
173
|
-
logger.error(`Dependency cycle detected: ${cycle.join(' → ')}`);
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
// Get runnable tasks
|
|
177
|
-
const runnable = graph.runnable();
|
|
178
|
-
if (runnable.length === 0) {
|
|
179
|
-
logger.info('No runnable tasks (all blocked by dependencies)');
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
// Check for phase transition
|
|
183
|
-
const currentPhase = state.currentPhase;
|
|
184
|
-
if (isPhaseComplete(tasks, currentPhase)) {
|
|
185
|
-
const next = getNextPhase(tasks);
|
|
186
|
-
if (next !== null && next !== currentPhase) {
|
|
187
|
-
logger.info(`Phase ${currentPhase} complete → advancing to Phase ${next}`);
|
|
188
|
-
state.currentPhase = next;
|
|
189
|
-
// Release for completed phase
|
|
190
|
-
const phaseTasks = tasks.filter(t => t.phase === currentPhase && t.status === 'done');
|
|
191
|
-
if (phaseTasks.length > 0) {
|
|
192
|
-
const release = createRelease(projectDir, phaseTasks, currentPhase);
|
|
193
|
-
if (release) {
|
|
194
|
-
logger.info(`Release ${release.version} created for Phase ${currentPhase}`);
|
|
195
|
-
appendEvent(projectDir, createEvent('release', { detail: release.version }));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
appendEvent(projectDir, createEvent('phase_transition', { detail: `Phase ${currentPhase} → ${next}` }));
|
|
199
|
-
writeState(projectDir, state);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Get parallel batches
|
|
203
|
-
const batches = getParallelBatches(runnable);
|
|
204
|
-
if (batches.length === 0)
|
|
205
|
-
return false;
|
|
206
|
-
const batch = batches[0];
|
|
207
|
-
const availableSlots = opts.concurrency - pool.activeCount();
|
|
208
|
-
const tasksToRun = batch.slice(0, availableSlots);
|
|
209
|
-
const queued = batch.length - tasksToRun.length;
|
|
210
|
-
if (tasksToRun.length === 0) {
|
|
211
|
-
logger.info(`All ${opts.concurrency} workers busy. ${batch.length} tasks queued.`);
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
if (queued > 0) {
|
|
215
|
-
logger.info(`Starting ${tasksToRun.length} tasks, ${queued} queued (waiting for workers)`);
|
|
216
|
-
}
|
|
217
165
|
// ─── Phase A: Check active PR watches (non-blocking) ───
|
|
166
|
+
// Must run BEFORE runnable/completion checks — active pipelines need servicing
|
|
167
|
+
// even when no new tasks are available (especially in interactive mode).
|
|
218
168
|
let workDone = false;
|
|
219
169
|
const prWatchIds = Object.keys(state.pipelines).filter(id => state.pipelines[id].step === 'pr-watch');
|
|
220
170
|
for (const taskId of prWatchIds) {
|
|
@@ -320,7 +270,56 @@ export async function runIteration(deps) {
|
|
|
320
270
|
}
|
|
321
271
|
writeState(projectDir, state);
|
|
322
272
|
}
|
|
323
|
-
//
|
|
273
|
+
// ─── Phase C: Pick and run new tasks ───
|
|
274
|
+
// Determine runnable tasks (skip if no tasks to evaluate)
|
|
275
|
+
let tasksToRun = [];
|
|
276
|
+
if (tasks.length > 0 && !isProjectComplete(tasks)) {
|
|
277
|
+
const graph = buildGraph(tasks);
|
|
278
|
+
const cycle = graph.findCycle();
|
|
279
|
+
if (cycle) {
|
|
280
|
+
logger.error(`Dependency cycle detected: ${cycle.join(' → ')}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const runnable = graph.runnable();
|
|
284
|
+
// Phase transition check
|
|
285
|
+
const currentPhase = state.currentPhase;
|
|
286
|
+
if (isPhaseComplete(tasks, currentPhase)) {
|
|
287
|
+
const next = getNextPhase(tasks);
|
|
288
|
+
if (next !== null && next !== currentPhase) {
|
|
289
|
+
logger.info(`Phase ${currentPhase} complete → advancing to Phase ${next}`);
|
|
290
|
+
state.currentPhase = next;
|
|
291
|
+
const phaseTasks = tasks.filter(t => t.phase === currentPhase && t.status === 'done');
|
|
292
|
+
if (phaseTasks.length > 0) {
|
|
293
|
+
const release = createRelease(projectDir, phaseTasks, currentPhase);
|
|
294
|
+
if (release) {
|
|
295
|
+
logger.info(`Release ${release.version} created for Phase ${currentPhase}`);
|
|
296
|
+
appendEvent(projectDir, createEvent('release', { detail: release.version }));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
appendEvent(projectDir, createEvent('phase_transition', { detail: `Phase ${currentPhase} → ${next}` }));
|
|
300
|
+
writeState(projectDir, state);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const batches = getParallelBatches(runnable);
|
|
304
|
+
if (batches.length > 0) {
|
|
305
|
+
const batch = batches[0];
|
|
306
|
+
const availableSlots = opts.concurrency - pool.activeCount();
|
|
307
|
+
tasksToRun = batch.slice(0, availableSlots);
|
|
308
|
+
const queued = batch.length - tasksToRun.length;
|
|
309
|
+
if (tasksToRun.length === 0) {
|
|
310
|
+
logger.info(`All ${opts.concurrency} workers busy. ${batch.length} tasks queued.`);
|
|
311
|
+
}
|
|
312
|
+
else if (queued > 0) {
|
|
313
|
+
logger.info(`Starting ${tasksToRun.length} tasks, ${queued} queued (waiting for workers)`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (tasks.length > 0) {
|
|
319
|
+
logger.info('All tasks complete.');
|
|
320
|
+
}
|
|
321
|
+
// Also consider active pipelines as "work in progress" (don't report idle)
|
|
322
|
+
const hasActivePipelines = Object.keys(state.pipelines).length > 0;
|
|
324
323
|
for (const task of tasksToRun) {
|
|
325
324
|
// Skip if already handled as recovered pipeline
|
|
326
325
|
if (state.pipelines[task.id])
|
|
@@ -375,7 +374,8 @@ export async function runIteration(deps) {
|
|
|
375
374
|
}
|
|
376
375
|
state.iteration++;
|
|
377
376
|
writeState(projectDir, state);
|
|
378
|
-
|
|
377
|
+
// Active pipelines (e.g. pr-watch) count as work even if no new tasks ran
|
|
378
|
+
return workDone || hasActivePipelines;
|
|
379
379
|
}
|
|
380
380
|
// ─── Pipeline execution ───
|
|
381
381
|
async function runTaskPipeline(task, workerId, agents, deps) {
|
|
@@ -651,7 +651,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
651
651
|
}),
|
|
652
652
|
model: getAgentModel(reviewRouting.agent, agents, task),
|
|
653
653
|
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
654
|
-
if (
|
|
654
|
+
if (isReviewPass(reviewResult.output)) {
|
|
655
655
|
reviewPassed = true;
|
|
656
656
|
}
|
|
657
657
|
else {
|
|
@@ -813,6 +813,21 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
815
|
// ─── Helpers ───
|
|
816
|
+
/**
|
|
817
|
+
* Detect whether a review output indicates PASS.
|
|
818
|
+
* Explicit "PASS" keyword OR structured review with no CRITICAL/WARN findings.
|
|
819
|
+
*/
|
|
820
|
+
function isReviewPass(output) {
|
|
821
|
+
if (/\bPASS\b/.test(output))
|
|
822
|
+
return true;
|
|
823
|
+
// Structured review: has GREEN section + CRITICAL/WARN are empty ("None", "None.", "N/A", or just whitespace after header)
|
|
824
|
+
const hasGreen = /###\s*GREEN/i.test(output);
|
|
825
|
+
const criticalEmpty = /###\s*CRITICAL\s*\n+(?:None\.?|N\/A|—|\s*\n)/i.test(output);
|
|
826
|
+
const warnEmpty = /###\s*WARN\s*\n+(?:None\.?|N\/A|—|\s*\n)/i.test(output);
|
|
827
|
+
if (hasGreen && criticalEmpty && warnEmpty)
|
|
828
|
+
return true;
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
816
831
|
async function spawnAgent(pool, slotId, opts, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers) {
|
|
817
832
|
const agentName = opts.agent ?? 'claude';
|
|
818
833
|
onSpawnStart?.(agentName);
|