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.
- package/dist/template/.claude/agents/devops-integrator.md +4 -2
- package/dist/template/.claude/agents/orchestrator.md +22 -0
- package/dist/template/.claude/docker/Dockerfile +0 -12
- package/dist/template/.claude/scripts/autonomous.mjs +280 -67
- package/dist/template/.claude/scripts/docker-run.mjs +7 -5
- package/package.json +1 -1
|
@@ -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
|
|
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: `
|
|
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
|
|
5
|
-
*
|
|
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>
|
|
12
|
-
* --max-turns <n>
|
|
13
|
-
* --delay <ms>
|
|
14
|
-
* --cooldown <ms>
|
|
15
|
-
* --project-dir <path>
|
|
16
|
-
* --skip-permissions
|
|
17
|
-
* --
|
|
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>
|
|
34
|
-
--max-turns <n>
|
|
35
|
-
--delay <ms>
|
|
36
|
-
--cooldown <ms>
|
|
37
|
-
--project-dir <path>
|
|
38
|
-
--skip-permissions
|
|
39
|
-
--
|
|
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
|
|
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');
|
|
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
|
-
// ───
|
|
234
|
+
// ─── Stream event display ───
|
|
192
235
|
|
|
193
|
-
function
|
|
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
|
-
//
|
|
201
|
-
const
|
|
202
|
-
child = spawn(`claude
|
|
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(
|
|
309
|
+
child.stdin.write(prompt);
|
|
208
310
|
child.stdin.end();
|
|
209
311
|
} else {
|
|
210
|
-
const args = [
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
299
|
-
if (
|
|
460
|
+
// ─── Rate limit handling ───
|
|
461
|
+
if (output.isRateLimit) {
|
|
300
462
|
let backoff = opts.cooldown;
|
|
301
|
-
const maxBackoff = 30 * 60_000;
|
|
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 (!
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
--login
|
|
138
|
-
--
|
|
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.');
|