create-claude-workspace 2.3.33 → 2.3.35
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/index.mjs +2 -1
- package/dist/scheduler/loop.mjs +71 -57
- package/dist/scheduler/ui/tui.mjs +61 -35
- package/package.json +1 -1
package/dist/scheduler/index.mjs
CHANGED
|
@@ -434,13 +434,14 @@ export async function runScheduler(opts) {
|
|
|
434
434
|
if (!workDone) {
|
|
435
435
|
if (state.taskMode === 'interactive') {
|
|
436
436
|
// Interactive mode: wait for user input (no external polling)
|
|
437
|
-
|
|
437
|
+
tui.setIdle(true);
|
|
438
438
|
while (!stopping) {
|
|
439
439
|
const pending = readInbox(opts.projectDir);
|
|
440
440
|
if (pending.length > 0 || discoveredIssueStore.peek().length > 0)
|
|
441
441
|
break;
|
|
442
442
|
await sleep(500, stoppingRef);
|
|
443
443
|
}
|
|
444
|
+
tui.setIdle(false);
|
|
444
445
|
}
|
|
445
446
|
else {
|
|
446
447
|
// No work → idle polling (platform/local)
|
package/dist/scheduler/loop.mjs
CHANGED
|
@@ -159,62 +159,11 @@ export async function runIteration(deps) {
|
|
|
159
159
|
task.status = 'skipped';
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
//
|
|
163
|
-
if (isProjectComplete(tasks)) {
|
|
164
|
-
logger.info('All tasks complete.');
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
// Scan agents
|
|
162
|
+
// Scan agents (needed by Phase A/B/C)
|
|
168
163
|
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
164
|
// ─── Phase A: Check active PR watches (non-blocking) ───
|
|
165
|
+
// Must run BEFORE runnable/completion checks — active pipelines need servicing
|
|
166
|
+
// even when no new tasks are available (especially in interactive mode).
|
|
218
167
|
let workDone = false;
|
|
219
168
|
const prWatchIds = Object.keys(state.pipelines).filter(id => state.pipelines[id].step === 'pr-watch');
|
|
220
169
|
for (const taskId of prWatchIds) {
|
|
@@ -320,7 +269,56 @@ export async function runIteration(deps) {
|
|
|
320
269
|
}
|
|
321
270
|
writeState(projectDir, state);
|
|
322
271
|
}
|
|
323
|
-
//
|
|
272
|
+
// ─── Phase C: Pick and run new tasks ───
|
|
273
|
+
// Determine runnable tasks (skip if no tasks to evaluate)
|
|
274
|
+
let tasksToRun = [];
|
|
275
|
+
if (tasks.length > 0 && !isProjectComplete(tasks)) {
|
|
276
|
+
const graph = buildGraph(tasks);
|
|
277
|
+
const cycle = graph.findCycle();
|
|
278
|
+
if (cycle) {
|
|
279
|
+
logger.error(`Dependency cycle detected: ${cycle.join(' → ')}`);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const runnable = graph.runnable();
|
|
283
|
+
// Phase transition check
|
|
284
|
+
const currentPhase = state.currentPhase;
|
|
285
|
+
if (isPhaseComplete(tasks, currentPhase)) {
|
|
286
|
+
const next = getNextPhase(tasks);
|
|
287
|
+
if (next !== null && next !== currentPhase) {
|
|
288
|
+
logger.info(`Phase ${currentPhase} complete → advancing to Phase ${next}`);
|
|
289
|
+
state.currentPhase = next;
|
|
290
|
+
const phaseTasks = tasks.filter(t => t.phase === currentPhase && t.status === 'done');
|
|
291
|
+
if (phaseTasks.length > 0) {
|
|
292
|
+
const release = createRelease(projectDir, phaseTasks, currentPhase);
|
|
293
|
+
if (release) {
|
|
294
|
+
logger.info(`Release ${release.version} created for Phase ${currentPhase}`);
|
|
295
|
+
appendEvent(projectDir, createEvent('release', { detail: release.version }));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
appendEvent(projectDir, createEvent('phase_transition', { detail: `Phase ${currentPhase} → ${next}` }));
|
|
299
|
+
writeState(projectDir, state);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const batches = getParallelBatches(runnable);
|
|
303
|
+
if (batches.length > 0) {
|
|
304
|
+
const batch = batches[0];
|
|
305
|
+
const availableSlots = opts.concurrency - pool.activeCount();
|
|
306
|
+
tasksToRun = batch.slice(0, availableSlots);
|
|
307
|
+
const queued = batch.length - tasksToRun.length;
|
|
308
|
+
if (tasksToRun.length === 0) {
|
|
309
|
+
logger.info(`All ${opts.concurrency} workers busy. ${batch.length} tasks queued.`);
|
|
310
|
+
}
|
|
311
|
+
else if (queued > 0) {
|
|
312
|
+
logger.info(`Starting ${tasksToRun.length} tasks, ${queued} queued (waiting for workers)`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (tasks.length > 0) {
|
|
318
|
+
logger.info('All tasks complete.');
|
|
319
|
+
}
|
|
320
|
+
// Also consider active pipelines as "work in progress" (don't report idle)
|
|
321
|
+
const hasActivePipelines = Object.keys(state.pipelines).length > 0;
|
|
324
322
|
for (const task of tasksToRun) {
|
|
325
323
|
// Skip if already handled as recovered pipeline
|
|
326
324
|
if (state.pipelines[task.id])
|
|
@@ -375,7 +373,8 @@ export async function runIteration(deps) {
|
|
|
375
373
|
}
|
|
376
374
|
state.iteration++;
|
|
377
375
|
writeState(projectDir, state);
|
|
378
|
-
|
|
376
|
+
// Active pipelines (e.g. pr-watch) count as work even if no new tasks ran
|
|
377
|
+
return workDone || hasActivePipelines;
|
|
379
378
|
}
|
|
380
379
|
// ─── Pipeline execution ───
|
|
381
380
|
async function runTaskPipeline(task, workerId, agents, deps) {
|
|
@@ -651,7 +650,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
651
650
|
}),
|
|
652
651
|
model: getAgentModel(reviewRouting.agent, agents, task),
|
|
653
652
|
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
654
|
-
if (
|
|
653
|
+
if (isReviewPass(reviewResult.output)) {
|
|
655
654
|
reviewPassed = true;
|
|
656
655
|
}
|
|
657
656
|
else {
|
|
@@ -813,6 +812,21 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
813
812
|
}
|
|
814
813
|
}
|
|
815
814
|
// ─── Helpers ───
|
|
815
|
+
/**
|
|
816
|
+
* Detect whether a review output indicates PASS.
|
|
817
|
+
* Explicit "PASS" keyword OR structured review with no CRITICAL/WARN findings.
|
|
818
|
+
*/
|
|
819
|
+
function isReviewPass(output) {
|
|
820
|
+
if (/\bPASS\b/.test(output))
|
|
821
|
+
return true;
|
|
822
|
+
// Structured review: has GREEN section + CRITICAL/WARN are empty ("None", "None.", "N/A", or just whitespace after header)
|
|
823
|
+
const hasGreen = /###\s*GREEN/i.test(output);
|
|
824
|
+
const criticalEmpty = /###\s*CRITICAL\s*\n+(?:None\.?|N\/A|—|\s*\n)/i.test(output);
|
|
825
|
+
const warnEmpty = /###\s*WARN\s*\n+(?:None\.?|N\/A|—|\s*\n)/i.test(output);
|
|
826
|
+
if (hasGreen && criticalEmpty && warnEmpty)
|
|
827
|
+
return true;
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
816
830
|
async function spawnAgent(pool, slotId, opts, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers) {
|
|
817
831
|
const agentName = opts.agent ?? 'claude';
|
|
818
832
|
onSpawnStart?.(agentName);
|
|
@@ -52,7 +52,7 @@ export class TUI {
|
|
|
52
52
|
this.state = {
|
|
53
53
|
iteration: 0, maxIter: 0, loopStart: Date.now(), iterStart: 0,
|
|
54
54
|
tools: 0, tokensIn: 0, tokensOut: 0, agents: [], taskName: '',
|
|
55
|
-
tasksDone: 0, tasksTotal: 0, paused: false, inputBuf: '',
|
|
55
|
+
tasksDone: 0, tasksTotal: 0, paused: false, idle: false, inputBuf: '',
|
|
56
56
|
pendingInputs: [],
|
|
57
57
|
};
|
|
58
58
|
if (this.interactive) {
|
|
@@ -157,13 +157,13 @@ export class TUI {
|
|
|
157
157
|
isPaused() { return this.state.paused; }
|
|
158
158
|
addPendingInput(text) {
|
|
159
159
|
this.state.pendingInputs.push(text);
|
|
160
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.magenta}
|
|
160
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.magenta}${BOLD}▶ USER${RESET} ${ANSI_COLORS.white}${text}${RESET} ${ANSI_COLORS.gray}[queued #${this.state.pendingInputs.length}]${RESET}`, ` ${ts()} ▶ USER ${text} [queued #${this.state.pendingInputs.length}]`);
|
|
161
161
|
this.renderStatusBar();
|
|
162
162
|
}
|
|
163
163
|
consumePendingInput() {
|
|
164
164
|
const item = this.state.pendingInputs.shift();
|
|
165
165
|
if (item) {
|
|
166
|
-
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green}
|
|
166
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green}${BOLD}▶ START${RESET} ${ANSI_COLORS.white}${item}${RESET}`, ` ${ts()} ▶ START ${item}`);
|
|
167
167
|
this.renderStatusBar();
|
|
168
168
|
}
|
|
169
169
|
return item;
|
|
@@ -194,24 +194,36 @@ export class TUI {
|
|
|
194
194
|
lines.push(` ${ANSI_COLORS.gray}› type to send input${RESET}`);
|
|
195
195
|
}
|
|
196
196
|
// Line 3: Status bar (stats)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
if (s.idle) {
|
|
198
|
+
// Idle: static bar, no running timers
|
|
199
|
+
let stats = `\x1b[7m ${ANSI_COLORS.gray}IDLE${RESET}\x1b[7m — waiting for input`;
|
|
200
|
+
if (s.pendingInputs.length > 0)
|
|
201
|
+
stats += ` | ${ANSI_COLORS.magenta}${s.pendingInputs.length} queued${RESET}\x1b[7m`;
|
|
202
|
+
if (s.paused)
|
|
203
|
+
stats += ` | ${ANSI_COLORS.yellow}⏸ PAUSED${RESET}\x1b[7m`;
|
|
204
|
+
stats += ` ${RESET}`;
|
|
205
|
+
lines.push(stats);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const elapsed = fmtDur(Date.now() - s.loopStart);
|
|
209
|
+
const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
|
|
210
|
+
const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
|
|
211
|
+
const filled = Math.round((pct / 100) * 8);
|
|
212
|
+
const bar = `${ANSI_COLORS.green}${'\u2588'.repeat(filled)}${ANSI_COLORS.gray}${'\u2591'.repeat(8 - filled)}${RESET}`;
|
|
213
|
+
const tok = fmtTok(s.tokensIn + s.tokensOut);
|
|
214
|
+
const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
|
|
215
|
+
let stats = `\x1b[7m ${elapsed} | Iter ${s.iteration}/${s.maxIter} \x1b[27m ${bar} \x1b[7m ${iterTime} | ${ANSI_COLORS.cyan}${s.tools}${RESET}\x1b[7m tools | ${ANSI_COLORS.yellow}${tok}${RESET}\x1b[7m tok`;
|
|
216
|
+
if (cur) {
|
|
217
|
+
const col = ANSI_COLORS[agentColor(cur)] || '';
|
|
218
|
+
stats += ` | ${col}${BOLD}${cur}${RESET}\x1b[7m`;
|
|
219
|
+
}
|
|
220
|
+
if (s.taskName)
|
|
221
|
+
stats += ` | ${ANSI_COLORS.cyan}${statusTrunc(s.taskName, 30)}${RESET}\x1b[7m`;
|
|
222
|
+
if (s.paused)
|
|
223
|
+
stats += ` | ${ANSI_COLORS.yellow}⏸ PAUSED${RESET}\x1b[7m`;
|
|
224
|
+
stats += ` ${RESET}`;
|
|
225
|
+
lines.push(stats);
|
|
208
226
|
}
|
|
209
|
-
if (s.taskName)
|
|
210
|
-
stats += ` | ${ANSI_COLORS.cyan}${statusTrunc(s.taskName, 30)}${RESET}\x1b[7m`;
|
|
211
|
-
if (s.paused)
|
|
212
|
-
stats += ` | ${ANSI_COLORS.yellow}⏸ PAUSED${RESET}\x1b[7m`;
|
|
213
|
-
stats += ` ${RESET}`;
|
|
214
|
-
lines.push(stats);
|
|
215
227
|
return lines;
|
|
216
228
|
}
|
|
217
229
|
/** Plain-text version for testing / non-ANSI contexts. */
|
|
@@ -224,21 +236,31 @@ export class TUI {
|
|
|
224
236
|
lines.push(` [${s.pendingInputs.length} queued] ${items}${more}`);
|
|
225
237
|
}
|
|
226
238
|
lines.push(s.inputBuf ? ` › ${s.inputBuf}` : ' › type to send input');
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
if (s.idle) {
|
|
240
|
+
let stats = ' IDLE — waiting for input';
|
|
241
|
+
if (s.pendingInputs.length > 0)
|
|
242
|
+
stats += ` | ${s.pendingInputs.length} queued`;
|
|
243
|
+
if (s.paused)
|
|
244
|
+
stats += ' | PAUSED';
|
|
245
|
+
lines.push(stats);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const elapsed = fmtDur(Date.now() - s.loopStart);
|
|
249
|
+
const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
|
|
250
|
+
const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
|
|
251
|
+
const filled = Math.round((pct / 100) * 8);
|
|
252
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(8 - filled);
|
|
253
|
+
const tok = fmtTok(s.tokensIn + s.tokensOut);
|
|
254
|
+
const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
|
|
255
|
+
let stats = ` ${elapsed} | Iter ${s.iteration}/${s.maxIter} ${bar} | ${iterTime} | ${s.tools} tools | ${tok} tok`;
|
|
256
|
+
if (cur)
|
|
257
|
+
stats += ` | ${cur}`;
|
|
258
|
+
if (s.taskName)
|
|
259
|
+
stats += ` | ${statusTrunc(s.taskName, 30)}`;
|
|
260
|
+
if (s.paused)
|
|
261
|
+
stats += ' | PAUSED';
|
|
262
|
+
lines.push(stats);
|
|
263
|
+
}
|
|
242
264
|
return lines;
|
|
243
265
|
}
|
|
244
266
|
clearStatusArea() {
|
|
@@ -321,6 +343,10 @@ export class TUI {
|
|
|
321
343
|
this.log(` ${BOLD}━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━${RESET}`);
|
|
322
344
|
this.log('');
|
|
323
345
|
}
|
|
346
|
+
setIdle(idle) {
|
|
347
|
+
this.state.idle = idle;
|
|
348
|
+
this.renderStatusBar();
|
|
349
|
+
}
|
|
324
350
|
setTask(name, done, total) {
|
|
325
351
|
this.state.taskName = name;
|
|
326
352
|
this.state.tasksDone = done;
|