create-claude-workspace 1.1.151 → 2.0.0
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/README.md +33 -1
- package/dist/index.js +29 -56
- package/dist/scheduler/agents/health-checker.mjs +98 -0
- package/dist/scheduler/agents/health-checker.spec.js +143 -0
- package/dist/scheduler/agents/orchestrator.mjs +149 -0
- package/dist/scheduler/agents/orchestrator.spec.js +87 -0
- package/dist/scheduler/agents/prompt-builder.mjs +204 -0
- package/dist/scheduler/agents/prompt-builder.spec.js +240 -0
- package/dist/scheduler/agents/worker-pool.mjs +137 -0
- package/dist/scheduler/agents/worker-pool.spec.js +45 -0
- package/dist/scheduler/git/ci-watcher.mjs +93 -0
- package/dist/scheduler/git/ci-watcher.spec.js +35 -0
- package/dist/scheduler/git/manager.mjs +228 -0
- package/dist/scheduler/git/manager.spec.js +198 -0
- package/dist/scheduler/git/release.mjs +117 -0
- package/dist/scheduler/git/release.spec.js +175 -0
- package/dist/scheduler/index.mjs +309 -0
- package/dist/scheduler/index.spec.js +72 -0
- package/dist/scheduler/integration.spec.js +289 -0
- package/dist/scheduler/loop.mjs +435 -0
- package/dist/scheduler/loop.spec.js +139 -0
- package/dist/scheduler/state/session.mjs +14 -0
- package/dist/scheduler/state/session.spec.js +36 -0
- package/dist/scheduler/state/state.mjs +102 -0
- package/dist/scheduler/state/state.spec.js +175 -0
- package/dist/scheduler/tasks/inbox.mjs +98 -0
- package/dist/scheduler/tasks/inbox.spec.js +168 -0
- package/dist/scheduler/tasks/parser.mjs +228 -0
- package/dist/scheduler/tasks/parser.spec.js +303 -0
- package/dist/scheduler/tasks/queue.mjs +152 -0
- package/dist/scheduler/tasks/queue.spec.js +223 -0
- package/dist/scheduler/types.mjs +20 -0
- package/dist/{scripts/lib → scheduler/ui}/tui.mjs +84 -41
- package/dist/{scripts/lib → scheduler/ui}/tui.spec.js +56 -0
- package/dist/scheduler/util/memory.mjs +126 -0
- package/dist/scheduler/util/memory.spec.js +165 -0
- package/dist/template/.claude/{profiles/angular.md → agents/angular-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/react.md → agents/react-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/svelte.md → agents/svelte-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/vue.md → agents/vue-engineer.md} +9 -4
- package/package.json +3 -4
- package/dist/scripts/autonomous.mjs +0 -492
- package/dist/scripts/autonomous.spec.js +0 -46
- package/dist/scripts/docker-run.mjs +0 -462
- package/dist/scripts/integration.spec.js +0 -108
- package/dist/scripts/lib/formatter.mjs +0 -309
- package/dist/scripts/lib/formatter.spec.js +0 -262
- package/dist/scripts/lib/state.mjs +0 -44
- package/dist/scripts/lib/state.spec.js +0 -59
- package/dist/template/.claude/docker/.dockerignore +0 -8
- package/dist/template/.claude/docker/Dockerfile +0 -54
- package/dist/template/.claude/docker/docker-compose.yml +0 -22
- package/dist/template/.claude/docker/docker-entrypoint.sh +0 -101
- /package/dist/{scripts/lib/types.mjs → scheduler/shared-types.mjs} +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.mjs +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.spec.js +0 -0
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ─── Autonomous development loop ───
|
|
3
|
-
// Headless runner for Claude Code orchestrator using Agent SDK.
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import { existsSync, writeFileSync, unlinkSync, readFileSync, readdirSync } from 'node:fs';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
7
|
-
import { DEFAULTS } from './lib/types.mjs';
|
|
8
|
-
import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mjs';
|
|
9
|
-
import { pollForNewWork } from './lib/idle-poll.mjs';
|
|
10
|
-
import { TUI } from './lib/tui.mjs';
|
|
11
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
-
import { config as dotenvConfig } from '@dotenvx/dotenvx';
|
|
13
|
-
// Disable SDK built-in agents (general-purpose, Explore, Plan, statusline-setup)
|
|
14
|
-
process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS = '1';
|
|
15
|
-
// ─── Args ───
|
|
16
|
-
function parseArgs(argv) {
|
|
17
|
-
const opts = { ...DEFAULTS };
|
|
18
|
-
const numFlag = (flag, i) => {
|
|
19
|
-
const val = argv[i + 1];
|
|
20
|
-
if (!val || val.startsWith('--')) {
|
|
21
|
-
console.error(`${flag} requires a value`);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
const n = parseInt(val, 10);
|
|
25
|
-
if (isNaN(n)) {
|
|
26
|
-
console.error(`${flag} must be a number`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
if (n < 0) {
|
|
30
|
-
console.error(`${flag} must be non-negative`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
33
|
-
return n;
|
|
34
|
-
};
|
|
35
|
-
const strFlag = (flag, i) => {
|
|
36
|
-
const val = argv[i + 1];
|
|
37
|
-
if (!val || val.startsWith('--')) {
|
|
38
|
-
console.error(`${flag} requires a value`);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
return val;
|
|
42
|
-
};
|
|
43
|
-
for (let i = 0; i < argv.length; i++) {
|
|
44
|
-
const arg = argv[i];
|
|
45
|
-
if (arg === '--help' || arg === '-h') {
|
|
46
|
-
opts.help = true;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (arg === '--skip-permissions') {
|
|
50
|
-
opts.skipPermissions = true;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (arg === '--no-lock') {
|
|
54
|
-
opts.noLock = true;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (arg === '--no-pull') {
|
|
58
|
-
opts.noPull = true;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (arg === '--dry-run') {
|
|
62
|
-
opts.dryRun = true;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (arg === '--resume') {
|
|
66
|
-
opts.resume = true;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (arg === '--interactive' || arg === '-i') {
|
|
70
|
-
opts.interactive = true;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (arg === '--max-iterations') {
|
|
74
|
-
opts.maxIterations = numFlag(arg, i);
|
|
75
|
-
i++;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (arg === '--max-turns') {
|
|
79
|
-
opts.maxTurns = numFlag(arg, i);
|
|
80
|
-
i++;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (arg === '--delay') {
|
|
84
|
-
opts.delay = numFlag(arg, i);
|
|
85
|
-
i++;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (arg === '--cooldown') {
|
|
89
|
-
opts.cooldown = numFlag(arg, i);
|
|
90
|
-
i++;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (arg === '--process-timeout') {
|
|
94
|
-
opts.processTimeout = numFlag(arg, i);
|
|
95
|
-
i++;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (arg === '--activity-timeout') {
|
|
99
|
-
opts.activityTimeout = numFlag(arg, i);
|
|
100
|
-
i++;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (arg === '--post-result-timeout') {
|
|
104
|
-
opts.postResultTimeout = numFlag(arg, i);
|
|
105
|
-
i++;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (arg === '--project-dir') {
|
|
109
|
-
opts.projectDir = resolve(strFlag(arg, i));
|
|
110
|
-
i++;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (arg === '--resume-session') {
|
|
114
|
-
opts.resumeSession = strFlag(arg, i);
|
|
115
|
-
i++;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (arg === '--notify-command') {
|
|
119
|
-
opts.notifyCommand = strFlag(arg, i);
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (arg === '--log-file') {
|
|
124
|
-
opts.logFile = strFlag(arg, i);
|
|
125
|
-
i++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (arg === '--idle-poll') {
|
|
129
|
-
opts.idlePollInterval = numFlag(arg, i);
|
|
130
|
-
i++;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (arg === '--max-idle') {
|
|
134
|
-
opts.maxIdleTime = numFlag(arg, i);
|
|
135
|
-
i++;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (arg.startsWith('--')) {
|
|
139
|
-
console.error(`Unknown option: ${arg}`);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return opts;
|
|
144
|
-
}
|
|
145
|
-
function printHelp() {
|
|
146
|
-
console.log(`
|
|
147
|
-
Autonomous development loop for Claude Code.
|
|
148
|
-
|
|
149
|
-
Usage: npx create-claude-workspace run [options]
|
|
150
|
-
|
|
151
|
-
Options:
|
|
152
|
-
--max-iterations <n> Max iterations (default: 50)
|
|
153
|
-
--max-turns <n> Max turns per Claude invocation (default: 50)
|
|
154
|
-
--delay <ms> Pause between tasks (default: 5000)
|
|
155
|
-
--cooldown <ms> Wait after error (default: 60000)
|
|
156
|
-
--process-timeout <ms> Max wall-clock time per invocation (default: 14400000 = 4h)
|
|
157
|
-
--activity-timeout <ms> Max silence before kill (default: 900000 = 15min)
|
|
158
|
-
--post-result-timeout <ms> Max wait after result (default: 30000 = 30s)
|
|
159
|
-
--project-dir <path> Project directory (default: cwd)
|
|
160
|
-
--skip-permissions Bypass all permission checks
|
|
161
|
-
--resume Resume from checkpoint state
|
|
162
|
-
--resume-session <id> Resume specific Claude session
|
|
163
|
-
--notify-command <cmd> Shell command on critical events
|
|
164
|
-
--log-file <path> Log file (default: .claude/autonomous.log)
|
|
165
|
-
--no-lock Disable lock file
|
|
166
|
-
--no-pull Skip auto git pull
|
|
167
|
-
--idle-poll <ms> Poll interval when idle (default: 300000 = 5min)
|
|
168
|
-
--max-idle <ms> Max idle time before exit, 0=unlimited (default: 0)
|
|
169
|
-
--interactive, -i Interactive TUI with input prompt
|
|
170
|
-
--dry-run Validate prerequisites only
|
|
171
|
-
--help Show this message
|
|
172
|
-
`);
|
|
173
|
-
}
|
|
174
|
-
// ─── Helpers ───
|
|
175
|
-
function sleep(ms, ref) {
|
|
176
|
-
return new Promise(r => {
|
|
177
|
-
const t = setTimeout(r, ms);
|
|
178
|
-
const check = setInterval(() => { if (ref.value) {
|
|
179
|
-
clearTimeout(t);
|
|
180
|
-
clearInterval(check);
|
|
181
|
-
r();
|
|
182
|
-
} }, 500);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
function formatDuration(ms) {
|
|
186
|
-
if (ms < 1000)
|
|
187
|
-
return `${ms}ms`;
|
|
188
|
-
if (ms < 60_000)
|
|
189
|
-
return `${(ms / 1000).toFixed(0)}s`;
|
|
190
|
-
return `${(ms / 60_000).toFixed(1)}min`;
|
|
191
|
-
}
|
|
192
|
-
function notify(cmd, type, message, iteration) {
|
|
193
|
-
if (!cmd)
|
|
194
|
-
return;
|
|
195
|
-
try {
|
|
196
|
-
execSync(cmd, { env: { ...process.env, RALPH_EVENT: type, RALPH_MESSAGE: message, RALPH_ITERATION: String(iteration) }, timeout: 10_000, stdio: 'pipe' });
|
|
197
|
-
}
|
|
198
|
-
catch { /* */ }
|
|
199
|
-
}
|
|
200
|
-
function checkAuth() {
|
|
201
|
-
if (process.env.ANTHROPIC_API_KEY)
|
|
202
|
-
return true;
|
|
203
|
-
try {
|
|
204
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
205
|
-
const creds = resolve(home, '.claude', '.credentials.json');
|
|
206
|
-
if (existsSync(creds)) {
|
|
207
|
-
const data = JSON.parse(readFileSync(creds, 'utf-8'));
|
|
208
|
-
if (data.claudeAiOauth?.accessToken)
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch { /* */ }
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
function acquireLock(dir, ui) {
|
|
216
|
-
const lockFile = resolve(dir, '.claude/autonomous.lock');
|
|
217
|
-
if (existsSync(lockFile)) {
|
|
218
|
-
ui.warn('Lock file exists. Another instance running?');
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
try {
|
|
222
|
-
writeFileSync(lockFile, String(process.pid));
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function releaseLock(dir) {
|
|
230
|
-
try {
|
|
231
|
-
unlinkSync(resolve(dir, '.claude/autonomous.lock'));
|
|
232
|
-
}
|
|
233
|
-
catch { /* */ }
|
|
234
|
-
}
|
|
235
|
-
function readMemory(dir) {
|
|
236
|
-
try {
|
|
237
|
-
return readFileSync(resolve(dir, 'MEMORY.md'), 'utf-8');
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return '';
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
function isProjectComplete(memory) {
|
|
244
|
-
// Only trust the orchestrator's explicit completion marker in MEMORY.md.
|
|
245
|
-
// Do NOT guess from TODO.md — the orchestrator handles phase transitions,
|
|
246
|
-
// product-owner re-evaluations, and task additions internally.
|
|
247
|
-
return /^Current Phase:.*PROJECT COMPLETE/mi.test(memory)
|
|
248
|
-
|| /^Project_Status:\s*complete/mi.test(memory);
|
|
249
|
-
}
|
|
250
|
-
function loadAgents(dir) {
|
|
251
|
-
const agentsDir = resolve(dir, '.claude', 'agents');
|
|
252
|
-
const agents = {};
|
|
253
|
-
if (!existsSync(agentsDir))
|
|
254
|
-
return agents;
|
|
255
|
-
for (const file of readdirSync(agentsDir)) {
|
|
256
|
-
if (!file.endsWith('.md'))
|
|
257
|
-
continue;
|
|
258
|
-
const name = file.replace('.md', '');
|
|
259
|
-
const content = readFileSync(resolve(agentsDir, file), 'utf-8');
|
|
260
|
-
// Parse YAML frontmatter
|
|
261
|
-
let description = name;
|
|
262
|
-
let model;
|
|
263
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
264
|
-
if (fmMatch) {
|
|
265
|
-
const fm = fmMatch[1];
|
|
266
|
-
const descMatch = fm.match(/description:\s*["']?(.*?)["']?\s*$/m);
|
|
267
|
-
if (descMatch)
|
|
268
|
-
description = descMatch[1];
|
|
269
|
-
const modelMatch = fm.match(/model:\s*(\S+)/m);
|
|
270
|
-
if (modelMatch)
|
|
271
|
-
model = modelMatch[1];
|
|
272
|
-
}
|
|
273
|
-
agents[name] = { description, prompt: content, model };
|
|
274
|
-
}
|
|
275
|
-
return agents;
|
|
276
|
-
}
|
|
277
|
-
function gitPull(dir) {
|
|
278
|
-
try {
|
|
279
|
-
execSync('git fetch --quiet && git pull --ff-only --quiet', { cwd: dir, timeout: 30_000, stdio: 'pipe' });
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// ─── Signal handling ───
|
|
287
|
-
let stopping = false;
|
|
288
|
-
const stoppingRef = { value: false };
|
|
289
|
-
// ─── Main loop ───
|
|
290
|
-
async function main() {
|
|
291
|
-
const opts = parseArgs(process.argv.slice(2));
|
|
292
|
-
if (opts.help) {
|
|
293
|
-
printHelp();
|
|
294
|
-
process.exit(0);
|
|
295
|
-
}
|
|
296
|
-
dotenvConfig({ path: resolve(opts.projectDir, '.env'), override: false, quiet: true });
|
|
297
|
-
const logPath = resolve(opts.projectDir, opts.logFile);
|
|
298
|
-
const tui = new TUI(logPath, opts.interactive);
|
|
299
|
-
tui.setTopAgent('orchestrator');
|
|
300
|
-
tui.banner();
|
|
301
|
-
tui.info(`Project: ${opts.projectDir}`);
|
|
302
|
-
tui.info(`Max iterations: ${opts.maxIterations} │ Max turns: ${opts.maxTurns}`);
|
|
303
|
-
// Auth check
|
|
304
|
-
if (!checkAuth()) {
|
|
305
|
-
tui.error('Not authenticated. Either:');
|
|
306
|
-
tui.error(' 1. Set ANTHROPIC_API_KEY env var (API billing)');
|
|
307
|
-
tui.error(' 2. Run `claude login` to authenticate with Claude Max plan');
|
|
308
|
-
tui.error(' For Docker: mount ~/.claude/.credentials.json into the container');
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
tui.success('Authentication verified');
|
|
312
|
-
// Lock
|
|
313
|
-
if (!opts.noLock && !acquireLock(opts.projectDir, tui)) {
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
// Git pull
|
|
317
|
-
if (!opts.noPull) {
|
|
318
|
-
if (gitPull(opts.projectDir))
|
|
319
|
-
tui.info('Git: pulled latest changes');
|
|
320
|
-
else
|
|
321
|
-
tui.warn('Git: pull failed or not a git repo');
|
|
322
|
-
}
|
|
323
|
-
if (opts.dryRun) {
|
|
324
|
-
tui.success('Dry run complete. All checks passed.');
|
|
325
|
-
if (!opts.noLock)
|
|
326
|
-
releaseLock(opts.projectDir);
|
|
327
|
-
process.exit(0);
|
|
328
|
-
}
|
|
329
|
-
// Checkpoint
|
|
330
|
-
let checkpoint;
|
|
331
|
-
const existing = readCheckpoint(opts.projectDir);
|
|
332
|
-
if (opts.resume && existing) {
|
|
333
|
-
checkpoint = existing;
|
|
334
|
-
tui.info(`Resuming from iteration ${checkpoint.iteration}`);
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
checkpoint = emptyCheckpoint();
|
|
338
|
-
}
|
|
339
|
-
// Signals
|
|
340
|
-
const cleanup = () => {
|
|
341
|
-
stopping = true;
|
|
342
|
-
stoppingRef.value = true;
|
|
343
|
-
if (!opts.noLock)
|
|
344
|
-
releaseLock(opts.projectDir);
|
|
345
|
-
writeCheckpoint(opts.projectDir, checkpoint);
|
|
346
|
-
tui.warn('Interrupted. State saved.');
|
|
347
|
-
process.exit(0);
|
|
348
|
-
};
|
|
349
|
-
process.on('SIGINT', cleanup);
|
|
350
|
-
process.on('SIGTERM', cleanup);
|
|
351
|
-
// Hotkey handler (interactive mode)
|
|
352
|
-
tui.setHotkeyHandler((action) => {
|
|
353
|
-
if (action === 'pause')
|
|
354
|
-
tui.warn('Paused. Press Ctrl+Z to resume.');
|
|
355
|
-
if (action === 'resume')
|
|
356
|
-
tui.success('Resumed.');
|
|
357
|
-
if (action === 'stop') {
|
|
358
|
-
stopping = true;
|
|
359
|
-
stoppingRef.value = true;
|
|
360
|
-
tui.warn('Stopping after current iteration...');
|
|
361
|
-
}
|
|
362
|
-
if (action === 'quit')
|
|
363
|
-
cleanup();
|
|
364
|
-
});
|
|
365
|
-
// ─── Loop ───
|
|
366
|
-
for (let i = checkpoint.iteration + 1; i <= opts.maxIterations && !stopping; i++) {
|
|
367
|
-
// Check completion → idle polling
|
|
368
|
-
const memory = readMemory(opts.projectDir);
|
|
369
|
-
if (isProjectComplete(memory)) {
|
|
370
|
-
if (!checkpoint.completionVerified) {
|
|
371
|
-
checkpoint.completionVerified = true;
|
|
372
|
-
writeCheckpoint(opts.projectDir, checkpoint);
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
tui.info('Project complete. Entering idle polling...');
|
|
376
|
-
let foundWork = false;
|
|
377
|
-
const idleStart = Date.now();
|
|
378
|
-
while (!stopping) {
|
|
379
|
-
const poll = await pollForNewWork(opts.projectDir, { info: (m) => tui.info(m), warn: (m) => tui.warn(m), error: (m) => tui.error(m), debug: () => { } });
|
|
380
|
-
if (poll.hasWork) {
|
|
381
|
-
tui.success(`New work detected (${poll.source}). Resuming...`);
|
|
382
|
-
checkpoint.completionVerified = false;
|
|
383
|
-
writeCheckpoint(opts.projectDir, checkpoint);
|
|
384
|
-
foundWork = true;
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
if (opts.maxIdleTime > 0 && Date.now() - idleStart >= opts.maxIdleTime) {
|
|
388
|
-
tui.info('Max idle time reached. Exiting.');
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
await sleep(opts.idlePollInterval, stoppingRef);
|
|
392
|
-
}
|
|
393
|
-
if (!foundWork)
|
|
394
|
-
break;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
tui.setIteration(i, opts.maxIterations);
|
|
399
|
-
tui.resetAgentStack();
|
|
400
|
-
// Run Claude via Agent SDK
|
|
401
|
-
try {
|
|
402
|
-
const resumeOpts = {};
|
|
403
|
-
if (opts.resumeSession) {
|
|
404
|
-
resumeOpts.resume = opts.resumeSession;
|
|
405
|
-
opts.resumeSession = null;
|
|
406
|
-
}
|
|
407
|
-
else if (opts.resume && checkpoint.lastSessionId) {
|
|
408
|
-
resumeOpts.continue = true;
|
|
409
|
-
opts.resume = false;
|
|
410
|
-
}
|
|
411
|
-
// Agents + CLAUDE.md loaded by SDK from .claude/ via settingSources: ['project']
|
|
412
|
-
const queryOptions = {
|
|
413
|
-
// Use settingSources to load agents from .claude/agents/ + CLAUDE.md
|
|
414
|
-
settingSources: ['project'],
|
|
415
|
-
agent: 'orchestrator',
|
|
416
|
-
model: 'claude-opus-4-6',
|
|
417
|
-
maxTurns: opts.maxTurns,
|
|
418
|
-
cwd: opts.projectDir,
|
|
419
|
-
};
|
|
420
|
-
if (opts.skipPermissions) {
|
|
421
|
-
queryOptions.permissionMode = 'bypassPermissions';
|
|
422
|
-
queryOptions.allowDangerouslySkipPermissions = true;
|
|
423
|
-
}
|
|
424
|
-
if (resumeOpts.continue)
|
|
425
|
-
queryOptions.continue = true;
|
|
426
|
-
if (resumeOpts.resume)
|
|
427
|
-
queryOptions.resume = resumeOpts.resume;
|
|
428
|
-
const q = query({ prompt: 'Continue autonomous development. Pick the next task and execute the full workflow.', options: queryOptions });
|
|
429
|
-
// Wire up interactive input → SDK message queue
|
|
430
|
-
let sessionId = '';
|
|
431
|
-
if (opts.interactive) {
|
|
432
|
-
tui.setInputHandler((text) => {
|
|
433
|
-
const msg = {
|
|
434
|
-
type: 'user',
|
|
435
|
-
message: { role: 'user', content: text },
|
|
436
|
-
parent_tool_use_id: null,
|
|
437
|
-
session_id: sessionId,
|
|
438
|
-
priority: 'next',
|
|
439
|
-
};
|
|
440
|
-
async function* single() { yield msg; }
|
|
441
|
-
q.streamInput(single()).catch(() => { });
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
for await (const message of q) {
|
|
445
|
-
tui.handleMessage(message);
|
|
446
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
447
|
-
sessionId = message.session_id || '';
|
|
448
|
-
}
|
|
449
|
-
if (message.type === 'result') {
|
|
450
|
-
checkpoint.lastSessionId = message.session_id ?? null;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
checkpoint.stats.iterations++;
|
|
454
|
-
tui.iterationEnd();
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
tui.error(`${err.message}`);
|
|
458
|
-
tui.warn(`Cooling down ${formatDuration(opts.cooldown)}...`);
|
|
459
|
-
await sleep(opts.cooldown, stoppingRef);
|
|
460
|
-
if (stopping)
|
|
461
|
-
break;
|
|
462
|
-
i--;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
checkpoint.iteration = i;
|
|
466
|
-
writeCheckpoint(opts.projectDir, checkpoint);
|
|
467
|
-
// Pause support
|
|
468
|
-
while (tui.isPaused() && !stopping) {
|
|
469
|
-
await sleep(1000, stoppingRef);
|
|
470
|
-
}
|
|
471
|
-
if (i < opts.maxIterations && !stopping) {
|
|
472
|
-
tui.info(`Next iteration in ${formatDuration(opts.delay)}...`);
|
|
473
|
-
await sleep(opts.delay, stoppingRef);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
// End
|
|
477
|
-
if (!opts.noLock)
|
|
478
|
-
releaseLock(opts.projectDir);
|
|
479
|
-
writeCheckpoint(opts.projectDir, checkpoint);
|
|
480
|
-
tui.success(`Loop ended after ${checkpoint.stats.iterations} iterations.`);
|
|
481
|
-
tui.destroy();
|
|
482
|
-
notify(opts.notifyCommand, 'completed', 'Loop ended', checkpoint.iteration);
|
|
483
|
-
}
|
|
484
|
-
// ─── Export for CLI integration ───
|
|
485
|
-
export { main as runAutonomousLoop, parseArgs, printHelp };
|
|
486
|
-
const isDirectRun = process.argv[1]?.replace(/\\/g, '/').endsWith('autonomous.mjs');
|
|
487
|
-
if (isDirectRun) {
|
|
488
|
-
main().catch(err => {
|
|
489
|
-
console.error('Fatal:', err);
|
|
490
|
-
process.exit(1);
|
|
491
|
-
});
|
|
492
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseArgs } from './autonomous.mjs';
|
|
3
|
-
describe('parseArgs', () => {
|
|
4
|
-
it('returns defaults for empty args', () => {
|
|
5
|
-
const opts = parseArgs([]);
|
|
6
|
-
expect(opts.maxIterations).toBe(50);
|
|
7
|
-
expect(opts.maxTurns).toBe(50);
|
|
8
|
-
expect(opts.skipPermissions).toBe(false);
|
|
9
|
-
expect(opts.dryRun).toBe(false);
|
|
10
|
-
expect(opts.resume).toBe(false);
|
|
11
|
-
});
|
|
12
|
-
it('parses boolean flags', () => {
|
|
13
|
-
const opts = parseArgs(['--skip-permissions', '--dry-run', '--resume', '--no-lock', '--no-pull']);
|
|
14
|
-
expect(opts.skipPermissions).toBe(true);
|
|
15
|
-
expect(opts.dryRun).toBe(true);
|
|
16
|
-
expect(opts.resume).toBe(true);
|
|
17
|
-
expect(opts.noLock).toBe(true);
|
|
18
|
-
expect(opts.noPull).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
it('parses numeric flags', () => {
|
|
21
|
-
const opts = parseArgs(['--max-iterations', '10', '--max-turns', '20', '--delay', '1000']);
|
|
22
|
-
expect(opts.maxIterations).toBe(10);
|
|
23
|
-
expect(opts.maxTurns).toBe(20);
|
|
24
|
-
expect(opts.delay).toBe(1000);
|
|
25
|
-
});
|
|
26
|
-
it('parses string flags', () => {
|
|
27
|
-
const opts = parseArgs(['--project-dir', '/tmp/proj', '--notify-command', 'echo hi', '--log-file', 'custom.log']);
|
|
28
|
-
expect(opts.projectDir).toContain('proj');
|
|
29
|
-
expect(opts.notifyCommand).toBe('echo hi');
|
|
30
|
-
expect(opts.logFile).toBe('custom.log');
|
|
31
|
-
});
|
|
32
|
-
it('sets help flag', () => {
|
|
33
|
-
expect(parseArgs(['--help']).help).toBe(true);
|
|
34
|
-
expect(parseArgs(['-h']).help).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
it('parses idle poll flags', () => {
|
|
37
|
-
const opts = parseArgs(['--idle-poll', '60000', '--max-idle', '3600000']);
|
|
38
|
-
expect(opts.idlePollInterval).toBe(60000);
|
|
39
|
-
expect(opts.maxIdleTime).toBe(3600000);
|
|
40
|
-
});
|
|
41
|
-
it('uses defaults for idle poll flags when not provided', () => {
|
|
42
|
-
const opts = parseArgs([]);
|
|
43
|
-
expect(opts.idlePollInterval).toBe(5 * 60_000);
|
|
44
|
-
expect(opts.maxIdleTime).toBe(0);
|
|
45
|
-
});
|
|
46
|
-
});
|