create-claude-workspace 2.3.34 → 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.
@@ -159,62 +159,11 @@ export async function runIteration(deps) {
159
159
  task.status = 'skipped';
160
160
  }
161
161
  }
162
- // Check project completion
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
- // Process new tasks through the pipeline
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
- return workDone;
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 (reviewResult.output.includes('**PASS**') || reviewResult.output.includes('PASS')) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.34",
3
+ "version": "2.3.35",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",