create-claude-workspace 2.3.30 → 2.3.32

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.
@@ -74,6 +74,14 @@ export function getPRStatus(cwd, platform, branch) {
74
74
  }
75
75
  return getGitLabMRStatus(cwd, branch);
76
76
  }
77
+ /** Like getPRStatus but uses the MR/PR number directly (more reliable than branch name lookup). */
78
+ export function getPRStatusByNumber(cwd, platform, prNumber) {
79
+ const id = String(prNumber);
80
+ if (platform === 'github') {
81
+ return getGitHubPRStatus(cwd, id);
82
+ }
83
+ return getGitLabMRStatus(cwd, id);
84
+ }
77
85
  function getGitHubPRStatus(cwd, branch) {
78
86
  const output = run('gh', [
79
87
  'pr', 'view', branch,
@@ -15,6 +15,7 @@ import { checkAuth } from './agents/health-checker.mjs';
15
15
  import { pollForNewWork } from './util/idle-poll.mjs';
16
16
  import { TUI } from './ui/tui.mjs';
17
17
  import { DiscoveredIssueStore } from './tools/report-issue.mjs';
18
+ import { readInbox, writeToInbox } from './tasks/inbox.mjs';
18
19
  // ─── Args ───
19
20
  export function parseSchedulerArgs(argv) {
20
21
  const opts = { ...SCHEDULER_DEFAULTS };
@@ -131,8 +132,8 @@ export function parseSchedulerArgs(argv) {
131
132
  }
132
133
  if (arg === '--task-mode') {
133
134
  const val = strFlag(arg, i);
134
- if (val !== 'local' && val !== 'platform') {
135
- console.error(`--task-mode must be 'local' or 'platform'`);
135
+ if (val !== 'local' && val !== 'platform' && val !== 'interactive') {
136
+ console.error(`--task-mode must be 'local', 'platform', or 'interactive'`);
136
137
  process.exit(1);
137
138
  }
138
139
  mutable.taskMode = val;
@@ -190,7 +191,8 @@ Options:
190
191
  --no-pull Skip auto git pull
191
192
  --idle-poll <ms> Poll interval when idle (default: 300000)
192
193
  --max-idle <ms> Max idle time before exit (default: 0 = unlimited)
193
- --interactive, -i Interactive TUI with input prompt
194
+ --task-mode <mode> Task source: local, platform, or interactive
195
+ --interactive, -i Interactive mode — tasks from user input only (implies --task-mode interactive)
194
196
  --dry-run Validate prerequisites only
195
197
  --help Show this message
196
198
  `);
@@ -225,6 +227,13 @@ export async function runScheduler(opts) {
225
227
  const logPath = resolve(opts.projectDir, opts.logFile);
226
228
  const tui = new TUI(logPath, opts.interactive);
227
229
  tui.setTopAgent('scheduler');
230
+ // Wire TUI input → inbox (interactive mode: user text becomes tasks)
231
+ if (opts.interactive) {
232
+ tui.setInputHandler((text) => {
233
+ writeToInbox(opts.projectDir, [{ type: 'add-task', title: text }]);
234
+ tui.info(`Task queued: ${text}`);
235
+ });
236
+ }
228
237
  const logger = {
229
238
  info: (m) => tui.info(m),
230
239
  warn: (m) => tui.warn(m),
@@ -304,7 +313,12 @@ export async function runScheduler(opts) {
304
313
  }
305
314
  // Task mode detection — always re-detect unless user explicitly set --task-mode
306
315
  // (stored state may be stale if CLI was installed/authenticated since last run)
307
- if (opts.taskMode) {
316
+ if (opts.interactive && !opts.taskMode) {
317
+ // Interactive flag implies interactive task mode (no issues, no TODO)
318
+ state.taskMode = 'interactive';
319
+ logger.info('Task mode: interactive (user input only)');
320
+ }
321
+ else if (opts.taskMode) {
308
322
  state.taskMode = opts.taskMode;
309
323
  logger.info(`Task mode (from flag): ${state.taskMode}`);
310
324
  }
@@ -369,6 +383,15 @@ export async function runScheduler(opts) {
369
383
  };
370
384
  process.on('SIGINT', cleanup);
371
385
  process.on('SIGTERM', cleanup);
386
+ // Wire TUI hotkeys → scheduler control
387
+ if (opts.interactive) {
388
+ tui.setHotkeyHandler((action) => {
389
+ if (action === 'stop' || action === 'quit') {
390
+ stopping = true;
391
+ stoppingRef.value = true;
392
+ }
393
+ });
394
+ }
372
395
  // ─── Loop ───
373
396
  for (let i = state.iteration; i < opts.maxIterations && !stopping; i++) {
374
397
  tui.setIteration(i + 1, opts.maxIterations);
@@ -386,21 +409,33 @@ export async function runScheduler(opts) {
386
409
  onSpawnEnd,
387
410
  });
388
411
  if (!workDone) {
389
- // No work idle polling
390
- logger.info('No work available. Entering idle polling...');
391
- const idleStart = Date.now();
392
- while (!stopping) {
393
- const poll = await pollForNewWork(opts.projectDir, logger, state.taskMode, state.completedTasks);
394
- if (poll.hasWork) {
395
- logger.info(`New work detected (${poll.source}). Resuming...`);
396
- break;
412
+ if (state.taskMode === 'interactive') {
413
+ // Interactive mode: wait for user input (no external polling)
414
+ logger.info('Waiting for input...');
415
+ while (!stopping) {
416
+ const pending = readInbox(opts.projectDir);
417
+ if (pending.length > 0 || discoveredIssueStore.peek().length > 0)
418
+ break;
419
+ await sleep(500, stoppingRef);
397
420
  }
398
- if (opts.maxIdleTime > 0 && Date.now() - idleStart >= opts.maxIdleTime) {
399
- logger.info('Max idle time reached. Exiting.');
400
- stopping = true;
401
- break;
421
+ }
422
+ else {
423
+ // No work → idle polling (platform/local)
424
+ logger.info('No work available. Entering idle polling...');
425
+ const idleStart = Date.now();
426
+ while (!stopping) {
427
+ const poll = await pollForNewWork(opts.projectDir, logger, state.taskMode, state.completedTasks);
428
+ if (poll.hasWork) {
429
+ logger.info(`New work detected (${poll.source}). Resuming...`);
430
+ break;
431
+ }
432
+ if (opts.maxIdleTime > 0 && Date.now() - idleStart >= opts.maxIdleTime) {
433
+ logger.info('Max idle time reached. Exiting.');
434
+ stopping = true;
435
+ break;
436
+ }
437
+ await sleep(opts.idlePollInterval, stoppingRef);
402
438
  }
403
- await sleep(opts.idlePollInterval, stoppingRef);
404
439
  }
405
440
  continue;
406
441
  }
@@ -10,7 +10,7 @@ import { buildGraph, getParallelBatches, isPhaseComplete, getNextPhase, isProjec
10
10
  import { writeState, appendEvent, createEvent, rotateLog } from './state/state.mjs';
11
11
  import { recordSession, getSession, clearSession } from './state/session.mjs';
12
12
  import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, cleanMainForMerge, popStash, getMainBranch, listWorktrees, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, deleteBranchRemote, cleanMergedBranches, pruneRemoteBranches, findStaleUnmergedBranches, } from './git/manager.mjs';
13
- import { createPR, getPRStatus, getPRComments, mergePR, closePR } from './git/pr-manager.mjs';
13
+ import { createPR, getPRStatus, getPRStatusByNumber, getPRComments, mergePR, closePR } from './git/pr-manager.mjs';
14
14
  import { scanAgents } from './agents/health-checker.mjs';
15
15
  import { detectCIPlatform, fetchFailureLogs } from './git/ci-watcher.mjs';
16
16
  import { createRelease } from './git/release.mjs';
@@ -79,9 +79,12 @@ export async function runIteration(deps) {
79
79
  state._recoveryDone = true;
80
80
  await recoverOrphanedWorktrees(projectDir, state, logger, deps);
81
81
  }
82
- // Load tasks (dual mode: platform issues or local TODO.md)
82
+ // Load tasks (triple mode: interactive = inbox only, platform = issues, local = TODO.md)
83
83
  let tasks;
84
- if (state.taskMode === 'platform') {
84
+ if (state.taskMode === 'interactive') {
85
+ tasks = []; // No external sources — only inbox + discovered
86
+ }
87
+ else if (state.taskMode === 'platform') {
85
88
  const platform = detectCIPlatform(projectDir);
86
89
  if (platform === 'none') {
87
90
  logger.error('Platform mode requires a git remote (github/gitlab)');
@@ -1230,7 +1233,8 @@ async function checkPRWatch(taskId, pipeline, projectDir, agents, deps) {
1230
1233
  return 'failed';
1231
1234
  const branch = pipeline.branchSlug;
1232
1235
  try {
1233
- const prStatus = getPRStatus(projectDir, ciPlatform, branch);
1236
+ // Use PR number (more reliable than branch name lookup which can fail on glab)
1237
+ const prStatus = getPRStatusByNumber(projectDir, ciPlatform, pipeline.prState.prNumber);
1234
1238
  if (prStatus.status === 'merged')
1235
1239
  return 'merged';
1236
1240
  // PR was closed (not merged) — try local merge as fallback before giving up
@@ -171,6 +171,10 @@ function readTodoSafe(projectDir) {
171
171
  }
172
172
  // ─── Main poll function ───
173
173
  export async function pollForNewWork(projectDir, log, taskMode = 'local', completedTasks = []) {
174
+ // Interactive mode: no external sources to poll — caller handles inbox directly
175
+ if (taskMode === 'interactive') {
176
+ return { hasWork: false, source: null, issueCount: 0 };
177
+ }
174
178
  // Platform mode: check issues directly via CLI
175
179
  if (taskMode === 'platform') {
176
180
  return pollPlatformIssues(projectDir, log, completedTasks);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.30",
3
+ "version": "2.3.32",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",