create-claude-workspace 1.1.78 → 1.1.80

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.
@@ -321,6 +321,13 @@ export function runClaude(opts, log, runOpts = {}) {
321
321
  const event = parseStreamEvent(line);
322
322
  if (!event) {
323
323
  log.debug(`Non-JSON: ${line.slice(0, 200)}`);
324
+ // Check non-JSON stdout lines for error signals (e.g. "You've hit your limit" printed as plain text)
325
+ const lower = line.toLowerCase();
326
+ detectTextSignals(lower);
327
+ // Append to stderr so parseUsageLimitResetMs can find reset time
328
+ if (USAGE_LIMIT_RE.test(lower) || RATE_LIMIT_RE.test(lower) || AUTH_ERROR_RE.test(lower)) {
329
+ stderr += line + '\n';
330
+ }
324
331
  continue;
325
332
  }
326
333
  // Extract session ID
@@ -400,7 +407,8 @@ export function runClaude(opts, log, runOpts = {}) {
400
407
  }
401
408
  // Flush remaining buffer
402
409
  if (buffer.trim()) {
403
- const event = parseStreamEvent(buffer.replace(/\r/g, ''));
410
+ const cleaned = buffer.replace(/\r/g, '');
411
+ const event = parseStreamEvent(cleaned);
404
412
  if (event) {
405
413
  if (event.session_id && !sessionId)
406
414
  sessionId = event.session_id;
@@ -409,6 +417,14 @@ export function runClaude(opts, log, runOpts = {}) {
409
417
  resultReceived = true;
410
418
  }
411
419
  }
420
+ else {
421
+ // Non-JSON buffer (e.g. "You've hit your limit" without trailing newline)
422
+ const lower = cleaned.toLowerCase();
423
+ detectTextSignals(lower);
424
+ if (USAGE_LIMIT_RE.test(lower) || RATE_LIMIT_RE.test(lower) || AUTH_ERROR_RE.test(lower)) {
425
+ stderr += cleaned + '\n';
426
+ }
427
+ }
412
428
  }
413
429
  // Final stderr-based detection
414
430
  detectTextSignals(stderr.toLowerCase());
@@ -424,6 +440,7 @@ export function runClaude(opts, log, runOpts = {}) {
424
440
  timedOut: killReason === 'process_timeout',
425
441
  activityTimedOut: killReason === 'activity_timeout',
426
442
  hasResult: resultReceived,
443
+ durationMs,
427
444
  });
428
445
  if (resolved)
429
446
  return;
@@ -49,7 +49,10 @@ export function classifyError(signals) {
49
49
  return category;
50
50
  }
51
51
  // Exit code 1 with no error indicators → likely max-turns
52
- if (signals.code === 1 && stderr.trim().length < 50)
52
+ // But only if process ran long enough (>10s). Instant exit (e.g. usage limit printed
53
+ // as plain text that wasn't caught) should NOT be classified as max-turns to prevent
54
+ // infinite restart loops with no backoff.
55
+ if (signals.code === 1 && stderr.trim().length < 50 && (signals.durationMs ?? 60_000) > 10_000)
53
56
  return 'max_turns';
54
57
  // Non-zero exit with content
55
58
  if (signals.code !== null && signals.code !== 0)
@@ -56,8 +56,11 @@ describe('classifyError', () => {
56
56
  it('detects context exhaustion from stderr', () => {
57
57
  expect(classifyError({ ...base, code: 1, stderr: 'context_length_exceeded' })).toBe('context_exhausted');
58
58
  });
59
- it('classifies exit code 1 with minimal stderr as max_turns', () => {
60
- expect(classifyError({ ...base, code: 1, stderr: '' })).toBe('max_turns');
59
+ it('classifies exit code 1 with minimal stderr as max_turns when process ran >10s', () => {
60
+ expect(classifyError({ ...base, code: 1, stderr: '', durationMs: 60_000 })).toBe('max_turns');
61
+ });
62
+ it('classifies instant exit (code 1, <10s, no stderr) as cli_crash not max_turns', () => {
63
+ expect(classifyError({ ...base, code: 1, stderr: '', durationMs: 1_000 })).toBe('cli_crash');
61
64
  });
62
65
  it('classifies exit code 1 with content as cli_crash', () => {
63
66
  expect(classifyError({ ...base, code: 1, stderr: 'Some unexpected internal error happened during execution blah blah' })).toBe('cli_crash');
@@ -674,9 +674,12 @@ describe('Loop integration: classifyError → getErrorAction → checkpoint', ()
674
674
  expect(classifyError({ ...base, code: 1, stderr: 'ENOTFOUND' })).toBe('network_error');
675
675
  expect(classifyError({ ...base, code: 1, stderr: 'context_length_exceeded' })).toBe('context_exhausted');
676
676
  });
677
- it('exit 1 with minimal stderr → max_turns', () => {
678
- expect(classifyError({ ...base, code: 1, stderr: '' })).toBe('max_turns');
679
- expect(classifyError({ ...base, code: 1, stderr: 'short' })).toBe('max_turns');
677
+ it('exit 1 with minimal stderr and long duration → max_turns', () => {
678
+ expect(classifyError({ ...base, code: 1, stderr: '', durationMs: 60_000 })).toBe('max_turns');
679
+ expect(classifyError({ ...base, code: 1, stderr: 'short', durationMs: 60_000 })).toBe('max_turns');
680
+ });
681
+ it('exit 1 with minimal stderr and instant exit → cli_crash (not max_turns)', () => {
682
+ expect(classifyError({ ...base, code: 1, stderr: '', durationMs: 2_000 })).toBe('cli_crash');
680
683
  });
681
684
  it('exit 1 with long stderr → cli_crash', () => {
682
685
  expect(classifyError({ ...base, code: 1, stderr: 'a'.repeat(60) })).toBe('cli_crash');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.78",
3
+ "version": "1.1.80",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {