create-claude-workspace 1.1.14 → 1.1.16

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.
@@ -22,11 +22,13 @@ Determine which platform to use:
22
22
  - **GitHub (`gh`)**:
23
23
  - Windows: `winget install GitHub.cli`
24
24
  - macOS: `brew install gh`
25
- - Linux: `sudo apt install gh` / `sudo dnf install gh`
25
+ - Linux/Docker: `curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /usr/share/keyrings/githubcli-archive-keyring.gpg > /dev/null && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt-get update && sudo apt-get install -y gh`
26
26
  - **GitLab (`glab`)**:
27
27
  - Windows: `winget install GLab.GLab`
28
28
  - macOS: `brew install glab`
29
- - Linux: `sudo apt install glab` / `sudo dnf install glab`
29
+ - Linux/Docker: `curl -fsSL "https://packages.gitlab.com/install/repositories/gitlab/glab/script.deb.sh" | sudo bash && sudo apt-get install -y glab`
30
+
31
+ **Docker note:** In Docker containers, install the CLI for the detected platform on first use. The installed binary persists in the `claude-home` volume across container restarts if installed to `~/.local/bin`.
30
32
 
31
33
  ## Operations
32
34
 
@@ -475,6 +475,26 @@ NEVER stay stuck. Escalation order:
475
475
  **C: DECIDE** — Pick best approach, log to MEMORY.md
476
476
  **D: SKIP (last resort)** — Only if blocker is objectively unresolvable. Revert changes (`git stash --include-untracked -m "REVERTED: [task title]"`), return to main (`git checkout main 2>/dev/null || git checkout master`), delete orphaned branch (`git branch -D feat/{slug}`). Then log everything in MEMORY.md Blockers section, mark task as `[~]` SKIPPED in TODO.md with reason, commit tracking on main (`git add TODO.md MEMORY.md && git commit -m "chore: mark task as skipped"`, push only if remote exists), move to next task.
477
477
 
478
+ ## End-of-Iteration Output
479
+
480
+ When running in autonomous mode (detected by the UNATTENDED prompt), your FINAL message MUST clearly convey the iteration outcome. The `--json-schema` flag enforces the structure — just write naturally and include:
481
+
482
+ - **status** — one of:
483
+ - `completed` — task finished successfully (committed, pushed, tracking updated)
484
+ - `needs_input` — cannot proceed without user-provided information (missing token, ambiguous requirement, auth expired). Specify what is needed in `action`.
485
+ - `blocked` — external blocker (CI failure, merge conflict needing human resolution, dependency unavailable). The loop will retry next iteration.
486
+ - `error` — unrecoverable error (all fix attempts exhausted, agent failures)
487
+ - **action** — what should happen next: `continue`, `needs_gitlab_token`, `needs_github_token`, `needs_user_decision`, `needs_api_key`, `retry_after_cooldown`, `auth_expired`, etc.
488
+ - **message** — human-readable summary of what happened (1-2 sentences)
489
+ - **task_completed** — the task name/ID if completed, or null
490
+ - **next_task** — the next task to pick up, or null
491
+
492
+ Examples:
493
+ - Task completed: `status: completed, action: continue, message: "Implemented user auth flow and pushed MR #42", task_completed: "#42 User auth", next_task: "#43 Dashboard"`
494
+ - Missing token: `status: needs_input, action: needs_gitlab_token, message: "git push failed — GITLAB_TOKEN is missing or expired. Please provide a valid token."`
495
+ - Build stuck: `status: error, action: retry_after_cooldown, message: "Build failed 3 times on dependency resolution. Exhausted fix attempts."`
496
+ - Blocked: `status: blocked, action: needs_user_decision, message: "Task #15 requires API design decision — REST vs GraphQL. Cannot proceed without guidance."`
497
+
478
498
  ## Rules
479
499
 
480
500
  - You are an ORCHESTRATOR — delegate to specialist agents, do not bypass them
@@ -489,3 +509,5 @@ NEVER stay stuck. Escalation order:
489
509
  - Priority: working code > perfect code. Production quality, not hacks.
490
510
  - **Token safety**: NEVER echo, log, cat, or write secrets (NPM_TOKEN, API keys, etc.) to terminal output, MEMORY.md, CLAUDE.md, or any tracked file. Tokens are read from `~/.npmrc` or environment variables automatically by tools — never handle them directly.
491
511
  - For independent tasks within the same phase, consider running architect agents in parallel (STEP 2) — but ONLY for tasks that don't share API contracts
512
+ - When running in UNATTENDED mode, your final message MUST clearly state the outcome status, what was completed, and what comes next — the CLI extracts this into structured JSON automatically
513
+ - If you detect auth/token issues (git push 401, npm 403, etc.), return `needs_input` status with the specific token/action needed — do NOT retry silently
@@ -4,18 +4,6 @@ RUN apt-get update && \
4
4
  apt-get install -y --no-install-recommends git ca-certificates curl gosu && \
5
5
  apt-get clean && rm -rf /var/lib/apt/lists/*
6
6
 
7
- # GitHub CLI (gh)
8
- RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
9
- -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
10
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
11
- > /etc/apt/sources.list.d/github-cli.list && \
12
- apt-get update && apt-get install -y --no-install-recommends gh && \
13
- apt-get clean && rm -rf /var/lib/apt/lists/*
14
-
15
- # GitLab CLI (glab)
16
- RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/permalink/latest/downloads/glab_$(dpkg --print-architecture).deb" \
17
- -o /tmp/glab.deb && \
18
- dpkg -i /tmp/glab.deb && rm /tmp/glab.deb
19
7
 
20
8
  # Non-root user — Claude Code refuses --dangerously-skip-permissions as root
21
9
  RUN useradd -m -s /bin/bash claude && \
@@ -1,46 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Autonomous development loop — runs Claude Code repeatedly with clean context.
5
- * Each invocation reads MEMORY.md to resume from where it left off.
4
+ * Autonomous development loop — runs Claude Code in headless mode with
5
+ * structured JSON communication. Each invocation streams real-time events
6
+ * and returns a structured result for the script to evaluate.
6
7
  *
7
8
  * Usage:
8
9
  * node .claude/scripts/autonomous.mjs [options]
9
10
  *
10
11
  * Options:
11
- * --max-iterations <n> Max iterations before stopping (default: 50)
12
- * --max-turns <n> Max turns per Claude invocation (default: 50)
13
- * --delay <ms> Delay between iterations in ms (default: 5000)
14
- * --cooldown <ms> Delay after error (default: 60000)
15
- * --project-dir <path> Project directory (default: cwd)
16
- * --skip-permissions Add --dangerously-skip-permissions to claude (required for unattended)
17
- * --help Show this help message
12
+ * --max-iterations <n> Max iterations before stopping (default: 50)
13
+ * --max-turns <n> Max turns per Claude invocation (default: 50)
14
+ * --delay <ms> Delay between iterations in ms (default: 5000)
15
+ * --cooldown <ms> Delay after error (default: 60000)
16
+ * --project-dir <path> Project directory (default: cwd)
17
+ * --skip-permissions Add --dangerously-skip-permissions to claude
18
+ * --resume-session <id> Resume a specific session by ID
19
+ * --help Show this help message
18
20
  */
19
21
 
20
22
  import { execSync, spawn } from 'node:child_process';
23
+ import { createInterface } from 'node:readline';
21
24
  import { readFile, writeFile } from 'node:fs/promises';
22
25
  import { resolve } from 'node:path';
23
26
 
24
27
  const PROMPT = 'Continue autonomous development according to CLAUDE.md. You are running in UNATTENDED autonomous mode — there is no human to respond. NEVER ask for confirmation or permission. Proceed with all actions autonomously. Create, modify, and delete files as needed without asking.';
25
28
 
29
+ const RESULT_SCHEMA = JSON.stringify({
30
+ type: 'object',
31
+ properties: {
32
+ status: { type: 'string', enum: ['completed', 'needs_input', 'blocked', 'error'] },
33
+ action: { type: 'string', description: 'What action is needed: continue, needs_gitlab_token, needs_user_decision, retry_after_cooldown, etc.' },
34
+ message: { type: 'string', description: 'Human-readable summary of what happened this iteration' },
35
+ task_completed: { type: ['string', 'null'], description: 'Name/ID of the completed task' },
36
+ next_task: { type: ['string', 'null'], description: 'Name/ID of the next task to pick up' },
37
+ },
38
+ required: ['status', 'action', 'message'],
39
+ });
40
+
41
+ // ─── Terminal colors ───
42
+
43
+ const C = {
44
+ dim: '\x1b[2m', bold: '\x1b[1m', reset: '\x1b[0m',
45
+ cyan: '\x1b[36m', yellow: '\x1b[33m', green: '\x1b[32m', red: '\x1b[31m', magenta: '\x1b[35m',
46
+ };
47
+
48
+ // ─── Helpers ───
49
+
26
50
  function printHelp() {
27
51
  console.log(`
28
- Autonomous development loop for Claude Code.
52
+ Autonomous development loop for Claude Code (headless mode).
53
+
54
+ Uses structured JSON communication — Claude streams real-time events
55
+ and returns a structured result that the script evaluates.
29
56
 
30
57
  Usage: node .claude/scripts/autonomous.mjs [options]
31
58
 
32
59
  Options:
33
- --max-iterations <n> Max task iterations (default: 50)
34
- --max-turns <n> Max turns per Claude invocation (default: 50)
35
- --delay <ms> Pause between tasks in ms (default: 5000)
36
- --cooldown <ms> Wait after error in ms (default: 60000)
37
- --project-dir <path> Target project directory (default: cwd)
38
- --skip-permissions Run claude with --dangerously-skip-permissions
39
- --help Show this message
60
+ --max-iterations <n> Max task iterations (default: 50)
61
+ --max-turns <n> Max turns per Claude invocation (default: 50)
62
+ --delay <ms> Pause between tasks in ms (default: 5000)
63
+ --cooldown <ms> Wait after error in ms (default: 60000)
64
+ --project-dir <path> Target project directory (default: cwd)
65
+ --skip-permissions Run claude with --dangerously-skip-permissions
66
+ --resume-session <id> Resume a previous session by ID
67
+ --help Show this message
40
68
 
41
69
  Examples:
42
70
  node .claude/scripts/autonomous.mjs --skip-permissions
43
- node .claude/scripts/autonomous.mjs --skip-permissions --max-iterations 20 --delay 10000
71
+ node .claude/scripts/autonomous.mjs --skip-permissions --max-iterations 20
72
+ node .claude/scripts/autonomous.mjs --resume-session abc123
44
73
  `);
45
74
  }
46
75
 
@@ -53,6 +82,7 @@ function parseArgs() {
53
82
  cooldown: 60_000,
54
83
  projectDir: process.cwd(),
55
84
  skipPermissions: false,
85
+ resumeSession: null,
56
86
  };
57
87
 
58
88
  for (let i = 0; i < args.length; i++) {
@@ -66,7 +96,8 @@ function parseArgs() {
66
96
  case '--max-turns':
67
97
  case '--delay':
68
98
  case '--cooldown':
69
- case '--project-dir': {
99
+ case '--project-dir':
100
+ case '--resume-session': {
70
101
  const flag = args[i];
71
102
  if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
72
103
  console.error(`Error: ${flag} requires a value`);
@@ -74,6 +105,7 @@ function parseArgs() {
74
105
  }
75
106
  const val = args[++i];
76
107
  if (flag === '--project-dir') { opts.projectDir = resolve(val); break; }
108
+ if (flag === '--resume-session') { opts.resumeSession = val; break; }
77
109
  const key = flag.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
78
110
  opts[key] = parseInt(val, 10);
79
111
  break;
@@ -118,7 +150,7 @@ function formatDuration(ms) {
118
150
  async function readMemory(projectDir) {
119
151
  try {
120
152
  const content = await readFile(resolve(projectDir, 'MEMORY.md'), 'utf-8');
121
- return content.replace(/\r\n/g, '\n'); // normalize CRLF for Windows
153
+ return content.replace(/\r\n/g, '\n');
122
154
  } catch {
123
155
  return null;
124
156
  }
@@ -149,7 +181,6 @@ function isProjectComplete(memory) {
149
181
 
150
182
  function getMemoryField(memory, field) {
151
183
  if (!memory) return null;
152
- // Match "## Field\nvalue" or "**Field:** value" or "- Field: value"
153
184
  const patterns = [
154
185
  new RegExp(`## ${field}\\s*\\n+\\s*(.+)`),
155
186
  new RegExp(`\\*\\*${field}:?\\*\\*:?\\s*(.+)`),
@@ -170,6 +201,18 @@ function getCurrentPhase(memory) {
170
201
  return getMemoryField(memory, 'Current Phase') ?? 'unknown';
171
202
  }
172
203
 
204
+ function promptUser(question) {
205
+ return new Promise((resolve) => {
206
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
207
+ rl.question(`\n${C.yellow}[INPUT NEEDED]${C.reset} ${question}\n> `, (answer) => {
208
+ rl.close();
209
+ resolve(answer);
210
+ });
211
+ });
212
+ }
213
+
214
+ // ─── Signal handling ───
215
+
173
216
  let currentChild = null;
174
217
  let stopping = false;
175
218
 
@@ -188,27 +231,85 @@ process.on('SIGTERM', () => {
188
231
  stopping = true;
189
232
  });
190
233
 
191
- // ─── Run Claude ───
234
+ // ─── Stream event display ───
192
235
 
193
- function runClaude(projectDir, opts) {
236
+ function formatStreamEvent(event) {
237
+ // Handle stream_event wrapper (from --include-partial-messages)
238
+ if (event.type === 'stream_event') {
239
+ const inner = event.event ?? event;
240
+ const delta = inner.delta;
241
+ if (delta?.type === 'text_delta' && delta.text) {
242
+ return delta.text; // raw text fragment for streaming display
243
+ }
244
+ return null;
245
+ }
246
+
247
+ // Assistant message
248
+ if (event.type === 'assistant') {
249
+ const msg = event.message;
250
+ if (!msg) return null;
251
+ if (msg.type === 'tool_use' || msg.tool) {
252
+ const name = msg.name ?? msg.tool ?? 'tool';
253
+ const input = msg.input ?? {};
254
+ const detail = input.command ?? input.file_path ?? input.pattern ?? input.query ?? '';
255
+ const short = typeof detail === 'string' ? detail.slice(0, 120) : '';
256
+ return `${C.cyan}▶ ${name}${C.reset}${short ? ` ${C.dim}${short}${C.reset}` : ''}\n`;
257
+ }
258
+ if (typeof msg.content === 'string' && msg.content) {
259
+ return `${C.bold}${msg.content}${C.reset}\n`;
260
+ }
261
+ return null;
262
+ }
263
+
264
+ // Tool result
265
+ if (event.type === 'result' && event.subtype === 'tool_result') {
266
+ const isErr = event.is_error;
267
+ const icon = isErr ? `${C.red}✗` : `${C.green}✓`;
268
+ const content = typeof event.content === 'string' ? event.content : '';
269
+ const preview = content.slice(0, 150).replace(/\n/g, ' ');
270
+ return `${icon} ${C.dim}${preview}${C.reset}\n`;
271
+ }
272
+
273
+ return null;
274
+ }
275
+
276
+ // ─── Run Claude (headless structured mode) ───
277
+
278
+ function runClaude(projectDir, opts, { resumeSessionId = null, resumePrompt = null } = {}) {
194
279
  return new Promise((resolvePromise) => {
195
- let child;
196
280
  const isWin = process.platform === 'win32';
281
+ const prompt = resumePrompt ?? PROMPT;
282
+ let child;
283
+
284
+ // Build CLI flags
285
+ const baseFlags = [];
286
+ baseFlags.push('-p');
287
+ baseFlags.push('--output-format', 'stream-json');
288
+ baseFlags.push('--verbose');
289
+ baseFlags.push('--max-turns', String(opts.maxTurns));
290
+ baseFlags.push('--json-schema', RESULT_SCHEMA);
291
+
292
+ if (resumeSessionId) {
293
+ baseFlags.push('--resume', resumeSessionId);
294
+ } else {
295
+ baseFlags.push('--agent', 'orchestrator');
296
+ }
297
+
298
+ if (opts.skipPermissions) baseFlags.push('--dangerously-skip-permissions');
197
299
 
198
300
  if (isWin) {
199
301
  // Windows: claude is a .cmd shim requiring shell execution.
200
- // cmd.exe mangles quoted arguments, so pipe prompt via stdin instead.
201
- const permFlag = opts.skipPermissions ? ' --dangerously-skip-permissions' : '';
202
- child = spawn(`claude -p --agent orchestrator --max-turns ${opts.maxTurns}${permFlag}`, [], {
302
+ // Build command string; prompt piped via stdin to avoid cmd.exe quoting issues.
303
+ const flagStr = baseFlags.join(' ');
304
+ child = spawn(`claude ${flagStr}`, [], {
203
305
  cwd: projectDir,
204
306
  stdio: ['pipe', 'pipe', 'pipe'],
205
307
  shell: true,
206
308
  });
207
- child.stdin.write(PROMPT);
309
+ child.stdin.write(prompt);
208
310
  child.stdin.end();
209
311
  } else {
210
- const args = ['-p', PROMPT, '--agent', 'orchestrator', '--max-turns', String(opts.maxTurns)];
211
- if (opts.skipPermissions) args.push('--dangerously-skip-permissions');
312
+ const args = [...baseFlags, prompt];
212
313
  child = spawn('claude', args, {
213
314
  cwd: projectDir,
214
315
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -217,36 +318,92 @@ function runClaude(projectDir, opts) {
217
318
 
218
319
  currentChild = child;
219
320
  let stderr = '';
321
+ let sessionId = null;
322
+ let structuredResult = null;
323
+ let isRateLimit = false;
324
+ let buffer = '';
325
+
326
+ child.stdout.on('data', (chunk) => {
327
+ buffer += chunk.toString();
328
+ const lines = buffer.split('\n');
329
+ buffer = lines.pop(); // keep incomplete last line
330
+
331
+ for (const line of lines) {
332
+ if (!line.trim()) continue;
333
+ try {
334
+ const event = JSON.parse(line);
335
+
336
+ // Extract session ID
337
+ if (event.session_id && !sessionId) {
338
+ sessionId = event.session_id;
339
+ }
220
340
 
221
- child.stdout.on('data', chunk => {
222
- process.stdout.write(chunk);
341
+ // Extract structured result from final event
342
+ if (event.type === 'result' && event.subtype !== 'tool_result') {
343
+ if (event.structured_output) {
344
+ structuredResult = event.structured_output;
345
+ } else if (event.result && typeof event.result === 'string') {
346
+ // Try to parse result text as JSON (fallback for non-schema mode)
347
+ try { structuredResult = JSON.parse(event.result); } catch { /* not JSON */ }
348
+ }
349
+ // Session ID also in final result
350
+ if (event.session_id) sessionId = event.session_id;
351
+ }
352
+
353
+ // Rate limit detection from stream events
354
+ if (event.type === 'error') {
355
+ const msg = (event.error?.message ?? event.message ?? '').toLowerCase();
356
+ if (msg.includes('rate limit') || msg.includes('rate_limit') || msg.includes('overloaded')) {
357
+ isRateLimit = true;
358
+ }
359
+ }
360
+
361
+ // Display formatted event
362
+ const formatted = formatStreamEvent(event);
363
+ if (formatted) process.stdout.write(formatted);
364
+ } catch {
365
+ // Non-JSON line — display as-is
366
+ process.stdout.write(line + '\n');
367
+ }
368
+ }
223
369
  });
224
370
 
225
- child.stderr.on('data', chunk => {
371
+ child.stderr.on('data', (chunk) => {
226
372
  const text = chunk.toString();
227
373
  stderr += text;
374
+ // Check stderr for rate limits too (belt and suspenders)
375
+ const lower = text.toLowerCase();
376
+ if (lower.includes('rate limit') || lower.includes('rate_limit') || lower.includes('overloaded')) {
377
+ isRateLimit = true;
378
+ }
228
379
  process.stderr.write(text);
229
380
  });
230
381
 
231
- child.on('close', code => {
382
+ child.on('close', (code) => {
383
+ // Flush remaining buffer
384
+ if (buffer.trim()) {
385
+ try {
386
+ const event = JSON.parse(buffer);
387
+ if (event.session_id && !sessionId) sessionId = event.session_id;
388
+ if (event.type === 'result' && event.structured_output) {
389
+ structuredResult = event.structured_output;
390
+ }
391
+ const formatted = formatStreamEvent(event);
392
+ if (formatted) process.stdout.write(formatted);
393
+ } catch { /* ignore */ }
394
+ }
232
395
  currentChild = null;
233
- resolvePromise({ code, stderr });
396
+ resolvePromise({ code, stderr, sessionId, result: structuredResult, isRateLimit });
234
397
  });
235
398
 
236
- child.on('error', err => {
399
+ child.on('error', (err) => {
237
400
  currentChild = null;
238
- resolvePromise({ code: 1, stderr: err.message });
401
+ resolvePromise({ code: 1, stderr: err.message, sessionId: null, result: null, isRateLimit: false });
239
402
  });
240
403
  });
241
404
  }
242
405
 
243
- function detectRateLimit(output) {
244
- const stderr = output.stderr.toLowerCase();
245
- return stderr.includes('rate limit') ||
246
- stderr.includes('rate_limit') ||
247
- stderr.includes('too many requests') ||
248
- stderr.includes('overloaded');
249
- }
406
+ // ─── Main loop ───
250
407
 
251
408
  async function main() {
252
409
  const opts = parseArgs();
@@ -260,12 +417,15 @@ async function main() {
260
417
  }
261
418
 
262
419
  const workDir = opts.projectDir;
420
+ let resumeSessionId = opts.resumeSession;
421
+ let resumePrompt = null;
263
422
 
264
- log('Autonomous development loop started');
423
+ log('Autonomous development loop started (headless structured mode)');
265
424
  log(`Project: ${workDir}`);
266
425
  log(`Max iterations: ${opts.maxIterations} | Max turns/iteration: ${opts.maxTurns}`);
267
426
  log(`Delay: ${opts.delay}ms | Cooldown: ${opts.cooldown}ms`);
268
427
  if (opts.skipPermissions) log('Permissions: skipped (--dangerously-skip-permissions)');
428
+ if (resumeSessionId) log(`Resuming session: ${resumeSessionId}`);
269
429
  log('---');
270
430
 
271
431
  for (let i = 1; i <= opts.maxIterations; i++) {
@@ -282,62 +442,115 @@ async function main() {
282
442
  }
283
443
 
284
444
  // Reset complexity counter — each invocation is a fresh context
285
- await resetComplexityCounter(workDir);
445
+ if (!resumeSessionId) await resetComplexityCounter(workDir);
286
446
 
287
447
  const phase = getCurrentPhase(memory);
288
448
  const task = getCurrentTask(memory);
289
449
  log(`Iteration ${i}/${opts.maxIterations} | Phase: ${phase} | Task: ${task}`);
290
450
 
291
- const result = await runClaude(workDir, opts);
451
+ const output = await runClaude(workDir, opts, { resumeSessionId, resumePrompt });
452
+ resumeSessionId = null;
453
+ resumePrompt = null;
292
454
 
293
455
  if (stopping) {
294
456
  log('Stopped by user signal.');
295
457
  break;
296
458
  }
297
459
 
298
- // Rate limit wait with exponential backoff, retry indefinitely (don't count as iteration)
299
- if (detectRateLimit(result)) {
460
+ // ─── Rate limit handling ───
461
+ if (output.isRateLimit) {
300
462
  let backoff = opts.cooldown;
301
- const maxBackoff = 30 * 60_000; // cap at 30 minutes
463
+ const maxBackoff = 30 * 60_000;
302
464
  while (!stopping) {
303
465
  log(`Rate limited. Waiting ${formatDuration(backoff)}...`);
304
466
  await sleep(backoff);
305
467
  if (stopping) break;
306
- // Probe with a short invocation to check if limit is lifted
307
468
  log('Checking if rate limit has cleared...');
308
469
  const probe = await runClaude(workDir, opts);
309
- if (!detectRateLimit(probe)) {
470
+ if (!probe.isRateLimit) {
310
471
  log('Rate limit cleared. Resuming.');
311
- // Process probe result as a normal iteration
312
- if (probe.code === 0) {
313
- log(`Iteration ${i} complete.`);
314
- } else {
315
- log(`Claude exited with code ${probe.code}.`);
316
- }
317
472
  break;
318
473
  }
319
474
  backoff = Math.min(backoff * 2, maxBackoff);
320
475
  }
321
- if (stopping) {
322
- log('Stopped by user signal.');
323
- break;
324
- }
476
+ if (stopping) { log('Stopped by user signal.'); break; }
325
477
  if (i < opts.maxIterations) {
326
478
  log(`Next iteration in ${opts.delay / 1000}s...`);
327
479
  await sleep(opts.delay);
328
480
  }
481
+ i--; // don't count rate-limited iteration
329
482
  continue;
330
483
  }
331
484
 
332
- if (result.code !== 0) {
333
- log(`Claude exited with code ${result.code}. Cooling down ${opts.cooldown / 1000}s...`);
334
- await sleep(opts.cooldown);
485
+ // ─── Evaluate structured result ───
486
+ const result = output.result;
487
+
488
+ if (!result) {
489
+ // No structured result — fall back to exit code
490
+ if (output.code !== 0) {
491
+ log(`Claude exited with code ${output.code} (no structured result). Cooling down...`);
492
+ await sleep(opts.cooldown);
493
+ continue;
494
+ }
495
+ log(`Iteration ${i} complete (no structured result — legacy mode).`);
496
+ if (i < opts.maxIterations) {
497
+ log(`Next iteration in ${opts.delay / 1000}s...`);
498
+ await sleep(opts.delay);
499
+ }
335
500
  continue;
336
501
  }
337
502
 
338
- log(`Iteration ${i} complete.`);
503
+ // Structured result available
504
+ log(`Status: ${result.status} | Action: ${result.action}`);
505
+ log(`Message: ${result.message}`);
506
+ if (result.task_completed) log(`Completed: ${result.task_completed}`);
507
+ if (result.next_task) log(`Next task: ${result.next_task}`);
508
+
509
+ switch (result.status) {
510
+ case 'completed':
511
+ log(`Iteration ${i} complete.`);
512
+ break;
513
+
514
+ case 'needs_input':
515
+ log(`${C.yellow}Input required: ${result.action}${C.reset}`);
516
+ if (process.stdin.isTTY) {
517
+ const userInput = await promptUser(result.message);
518
+ if (userInput.trim()) {
519
+ resumeSessionId = output.sessionId;
520
+ resumePrompt = userInput;
521
+ log(`Resuming session ${output.sessionId} with user input...`);
522
+ i--; // don't count as iteration — we're continuing the same task
523
+ } else {
524
+ log('Empty input. Skipping to next iteration.');
525
+ }
526
+ } else {
527
+ log('Non-interactive mode. Cannot prompt for input.');
528
+ if (output.sessionId) {
529
+ log(`To resume manually: node .claude/scripts/autonomous.mjs --resume-session ${output.sessionId}`);
530
+ }
531
+ await sleep(opts.cooldown);
532
+ }
533
+ break;
534
+
535
+ case 'blocked':
536
+ log(`${C.red}BLOCKED: ${result.action}${C.reset}`);
537
+ log('Waiting for resolution...');
538
+ if (output.sessionId) {
539
+ log(`Session: ${output.sessionId}`);
540
+ }
541
+ await sleep(opts.cooldown);
542
+ break;
543
+
544
+ case 'error':
545
+ log(`${C.red}Error: ${result.action}${C.reset}`);
546
+ await sleep(opts.cooldown);
547
+ continue;
548
+
549
+ default:
550
+ log(`Unknown status: ${result.status}. Continuing...`);
551
+ }
339
552
 
340
- if (i < opts.maxIterations) {
553
+ if (i < opts.maxIterations && result.status !== 'needs_input') {
341
554
  log(`Next iteration in ${opts.delay / 1000}s...`);
342
555
  await sleep(opts.delay);
343
556
  }
@@ -92,7 +92,7 @@ function parseArgs() {
92
92
  const args = process.argv.slice(2);
93
93
  const opts = {
94
94
  shell: false, rebuild: false, login: false, help: false,
95
- maxIterations: '', maxTurns: '', delay: '', cooldown: '',
95
+ maxIterations: '', maxTurns: '', delay: '', cooldown: '', resumeSession: '',
96
96
  };
97
97
 
98
98
  for (let i = 0; i < args.length; i++) {
@@ -101,7 +101,7 @@ function parseArgs() {
101
101
  case '--shell': opts.shell = true; break;
102
102
  case '--rebuild': opts.rebuild = true; break;
103
103
  case '--login': opts.login = true; break;
104
- case '--max-iterations': case '--max-turns': case '--delay': case '--cooldown': {
104
+ case '--max-iterations': case '--max-turns': case '--delay': case '--cooldown': case '--resume-session': {
105
105
  const flag = args[i];
106
106
  if (i + 1 >= args.length || args[i + 1].startsWith('--')) {
107
107
  error(`${flag} requires a value`); process.exit(1);
@@ -133,9 +133,10 @@ Options:
133
133
  --max-turns <n> Max turns per iteration (default: 50)
134
134
  --delay <ms> Pause between tasks (default: 5000)
135
135
  --cooldown <ms> Wait after rate limit (default: 60000)
136
- --rebuild Force rebuild Docker image
137
- --login Run 'claude auth login' on host before starting container
138
- --help, -h Show this help
136
+ --rebuild Force rebuild Docker image
137
+ --login Run 'claude auth login' on host before starting container
138
+ --resume-session <id> Resume a previous Claude session by ID
139
+ --help, -h Show this help
139
140
 
140
141
  Authentication:
141
142
  The container uses your HOST machine's Claude auth automatically.
@@ -358,6 +359,7 @@ if (opts.shell) {
358
359
  if (opts.maxTurns) autoArgs += ` --max-turns ${opts.maxTurns}`;
359
360
  if (opts.delay) autoArgs += ` --delay ${opts.delay}`;
360
361
  if (opts.cooldown) autoArgs += ` --cooldown ${opts.cooldown}`;
362
+ if (opts.resumeSession) autoArgs += ` --resume-session ${opts.resumeSession}`;
361
363
  console.log('');
362
364
  info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
363
365
  info('Press Ctrl+C to stop after current iteration.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {