create-claude-workspace 1.1.45 → 1.1.47
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/index.js +30 -7
- package/dist/{template/.claude/scripts → scripts}/autonomous.mjs +0 -1
- package/dist/scripts/autonomous.spec.js +36 -0
- package/dist/{template/.claude/scripts → scripts}/docker-run.mjs +4 -6
- package/dist/scripts/integration.spec.js +297 -0
- package/dist/scripts/lib/claude-runner.spec.js +238 -0
- package/dist/scripts/lib/errors.spec.js +192 -0
- package/dist/scripts/lib/logger.spec.js +56 -0
- package/dist/scripts/lib/loop-continuation.integration.spec.js +205 -0
- package/dist/scripts/lib/loop-integration.spec.js +798 -0
- package/dist/scripts/lib/oauth-refresh.spec.js +126 -0
- package/dist/scripts/lib/state.spec.js +88 -0
- package/dist/scripts/lib/utils.spec.js +184 -0
- package/dist/template/.claude/CLAUDE.md +4 -1
- package/dist/template/.claude/agents/backend-ts-architect.md +15 -10
- package/dist/template/.claude/agents/orchestrator.md +33 -2
- package/dist/template/.claude/agents/ui-engineer.md +1 -1
- package/dist/template/.claude/templates/claude-md.md +51 -22
- package/package.json +2 -1
- /package/dist/{template/.claude/scripts → scripts}/lib/claude-runner.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/errors.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/logger.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/oauth-refresh.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/state.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/types.mjs +0 -0
- /package/dist/{template/.claude/scripts → scripts}/lib/utils.mjs +0 -0
package/dist/index.js
CHANGED
|
@@ -91,10 +91,11 @@ ${C.b}Examples:${C.n}
|
|
|
91
91
|
|
|
92
92
|
${C.b}What it creates:${C.n}
|
|
93
93
|
.claude/agents/ 10 specialist agents (orchestrator, architects, etc.)
|
|
94
|
-
.claude/scripts/ Autonomous loop + Docker runner
|
|
95
94
|
.claude/templates/ CLAUDE.md template for project initialization
|
|
95
|
+
.claude/profiles/ Frontend framework profiles (Angular, React, Vue, Svelte)
|
|
96
96
|
.claude/CLAUDE.md Agent routing instructions
|
|
97
97
|
.claude/docker/ Docker config (Dockerfile, compose, entrypoint)
|
|
98
|
+
.claude/.kit-version Kit version (for upgrade detection)
|
|
98
99
|
`);
|
|
99
100
|
}
|
|
100
101
|
// ─── Prompts ───
|
|
@@ -113,22 +114,37 @@ async function confirm(question) {
|
|
|
113
114
|
return answer === '' || answer.toLowerCase().startsWith('y');
|
|
114
115
|
}
|
|
115
116
|
// ─── File operations ───
|
|
116
|
-
function listFiles(dir, base = dir) {
|
|
117
|
+
function listFiles(dir, base = dir, skipDirs = []) {
|
|
117
118
|
const results = [];
|
|
118
119
|
for (const entry of readdirSync(dir)) {
|
|
119
120
|
const full = join(dir, entry);
|
|
121
|
+
const rel = relative(base, full);
|
|
120
122
|
if (statSync(full).isDirectory()) {
|
|
121
|
-
|
|
123
|
+
if (skipDirs.some(s => rel.replace(/\\/g, '/').startsWith(s)))
|
|
124
|
+
continue;
|
|
125
|
+
results.push(...listFiles(full, base, skipDirs));
|
|
122
126
|
}
|
|
123
127
|
else {
|
|
124
|
-
results.push(
|
|
128
|
+
results.push(rel);
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
return results;
|
|
128
132
|
}
|
|
133
|
+
function writeKitVersion(targetDir) {
|
|
134
|
+
const pkgPath = resolve(__dirname, '..', 'package.json');
|
|
135
|
+
if (existsSync(pkgPath)) {
|
|
136
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
137
|
+
const versionFile = join(targetDir, '.claude', '.kit-version');
|
|
138
|
+
const dir = dirname(versionFile);
|
|
139
|
+
if (!existsSync(dir))
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
writeFileSync(versionFile, pkg.version);
|
|
142
|
+
info(`Kit version: ${pkg.version}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
129
145
|
function ensureGitignore(targetDir) {
|
|
130
146
|
const gitignorePath = join(targetDir, '.gitignore');
|
|
131
|
-
const entries = ['.npmrc', '.docker-compose.auth.yml', '.claude/autonomous.log', '.claude/autonomous.lock', '.claude/autonomous-state.json', '.worktrees/'];
|
|
147
|
+
const entries = ['.npmrc', '.docker-compose.auth.yml', '.claude/autonomous.log', '.claude/autonomous.lock', '.claude/autonomous-state.json', '.claude/.kit-version', '.worktrees/'];
|
|
132
148
|
if (existsSync(gitignorePath)) {
|
|
133
149
|
let content = readFileSync(gitignorePath, 'utf-8');
|
|
134
150
|
const added = [];
|
|
@@ -148,7 +164,11 @@ function ensureGitignore(targetDir) {
|
|
|
148
164
|
}
|
|
149
165
|
// ─── Run ───
|
|
150
166
|
function runAutonomous(targetDir, docker, extraArgs = []) {
|
|
151
|
-
|
|
167
|
+
// Run scripts from the package itself, not from .claude/scripts/ in target dir
|
|
168
|
+
const scriptsDir = resolve(__dirname, 'scripts');
|
|
169
|
+
const script = docker
|
|
170
|
+
? resolve(scriptsDir, 'docker-run.mjs')
|
|
171
|
+
: resolve(scriptsDir, 'autonomous.mjs');
|
|
152
172
|
const args = docker ? [script, ...extraArgs] : [script, '--skip-permissions', ...extraArgs];
|
|
153
173
|
if (docker) {
|
|
154
174
|
info('Starting Docker-based autonomous development...');
|
|
@@ -214,7 +234,8 @@ async function main() {
|
|
|
214
234
|
if (!existsSync(targetDir)) {
|
|
215
235
|
mkdirSync(targetDir, { recursive: true });
|
|
216
236
|
}
|
|
217
|
-
|
|
237
|
+
// Scripts run from the package (node_modules), not from .claude/scripts/ in the target
|
|
238
|
+
const files = listFiles(TEMPLATE_DIR, TEMPLATE_DIR, ['.claude/scripts']);
|
|
218
239
|
let copied = 0;
|
|
219
240
|
let skipped = 0;
|
|
220
241
|
for (const file of files) {
|
|
@@ -235,6 +256,8 @@ async function main() {
|
|
|
235
256
|
info(`Copied ${copied} files`);
|
|
236
257
|
if (skipped > 0)
|
|
237
258
|
info(`Skipped ${skipped} existing files`);
|
|
259
|
+
// Write kit version for orchestrator to detect upgrades
|
|
260
|
+
writeKitVersion(targetDir);
|
|
238
261
|
ensureGitignore(targetDir);
|
|
239
262
|
console.log('');
|
|
240
263
|
info(`${C.b}Done!${C.n} Your project now has Claude Code agents.`);
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
});
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
// Single cross-platform script. Builds container, sets up auth, runs autonomous loop.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
6
|
-
import { join
|
|
6
|
+
import { join } from 'node:path';
|
|
7
7
|
import { homedir, platform as osPlatform, tmpdir } from 'node:os';
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const PROJECT_DIR = resolve(__dirname, '../..');
|
|
8
|
+
// PROJECT_DIR = target project (set by index.ts via cwd), not the npm package
|
|
9
|
+
const PROJECT_DIR = process.cwd();
|
|
11
10
|
const DOCKER_DIR = join(PROJECT_DIR, '.claude', 'docker');
|
|
12
11
|
const AUTH_COMPOSE = join(tmpdir(), 'claude-starter-kit-auth-compose.yml');
|
|
13
12
|
const IS_WIN = osPlatform() === 'win32';
|
|
@@ -120,7 +119,6 @@ One command to start safe, isolated autonomous development.
|
|
|
120
119
|
|
|
121
120
|
Usage:
|
|
122
121
|
npx create-claude-workspace docker [options]
|
|
123
|
-
node .claude/scripts/docker-run.mjs [options]
|
|
124
122
|
|
|
125
123
|
Options:
|
|
126
124
|
--shell Interactive shell instead of autonomous loop
|
|
@@ -342,7 +340,7 @@ function main() {
|
|
|
342
340
|
info('Autonomous mode — isolated in Docker, --skip-permissions is safe.');
|
|
343
341
|
info('Press Ctrl+C to stop after current iteration.');
|
|
344
342
|
console.log('');
|
|
345
|
-
compose(['run', '--rm', '-T', 'claude', '-c', `
|
|
343
|
+
compose(['run', '--rm', '-T', 'claude', '-c', `npx create-claude-workspace run ${escaped.join(' ')}`]);
|
|
346
344
|
}
|
|
347
345
|
}
|
|
348
346
|
export { main as runDockerLoop, parseArgs, printHelp };
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// ─── Integration tests: real Claude CLI invocations ───
|
|
2
|
+
// These tests call the actual Claude CLI. They require:
|
|
3
|
+
// - Claude CLI installed and on PATH
|
|
4
|
+
// - Valid authentication (API key or OAuth)
|
|
5
|
+
// - Network access to Anthropic API
|
|
6
|
+
//
|
|
7
|
+
// Run OUTSIDE Claude Code session (CLAUDECODE env var blocks nested sessions):
|
|
8
|
+
// npx vitest run src/scripts/integration.spec.ts --config vitest.integration.config.ts
|
|
9
|
+
//
|
|
10
|
+
// Or unset CLAUDECODE first:
|
|
11
|
+
// CLAUDECODE= npx vitest run src/scripts/integration.spec.ts --config vitest.integration.config.ts
|
|
12
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
14
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { runClaude } from './lib/claude-runner.mjs';
|
|
18
|
+
import { checkClaudeInstalled, checkAuth } from './lib/utils.mjs';
|
|
19
|
+
import { DEFAULTS } from './lib/types.mjs';
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PROJECT_ROOT = resolve(__dirname, '../..');
|
|
22
|
+
const TEST_DIR = resolve(__dirname, '__integration_tmp__');
|
|
23
|
+
const SHELL = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
24
|
+
// Detect nested Claude Code session — CLI refuses to launch inside another session
|
|
25
|
+
const isNestedSession = !!process.env.CLAUDECODE;
|
|
26
|
+
const nopLog = {
|
|
27
|
+
info: () => { },
|
|
28
|
+
warn: () => { },
|
|
29
|
+
error: () => { },
|
|
30
|
+
debug: () => { },
|
|
31
|
+
};
|
|
32
|
+
const captureLog = () => {
|
|
33
|
+
const lines = [];
|
|
34
|
+
return {
|
|
35
|
+
lines,
|
|
36
|
+
log: {
|
|
37
|
+
info: (m) => lines.push(`[INFO] ${m}`),
|
|
38
|
+
warn: (m) => lines.push(`[WARN] ${m}`),
|
|
39
|
+
error: (m) => lines.push(`[ERROR] ${m}`),
|
|
40
|
+
debug: (m) => lines.push(`[DEBUG] ${m}`),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
function makeOpts(overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
...DEFAULTS,
|
|
47
|
+
projectDir: TEST_DIR,
|
|
48
|
+
maxTurns: 1,
|
|
49
|
+
processTimeout: 60_000,
|
|
50
|
+
activityTimeout: 30_000,
|
|
51
|
+
postResultTimeout: 10_000,
|
|
52
|
+
skipPermissions: true,
|
|
53
|
+
noLock: true,
|
|
54
|
+
noPull: true,
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ─── Pre-check: skip all if Claude not available ───
|
|
59
|
+
let claudeAvailable = false;
|
|
60
|
+
beforeAll(() => {
|
|
61
|
+
claudeAvailable = checkClaudeInstalled() && checkAuth();
|
|
62
|
+
if (!claudeAvailable) {
|
|
63
|
+
console.warn('⚠ Claude CLI not available or not authenticated — skipping integration tests');
|
|
64
|
+
}
|
|
65
|
+
if (isNestedSession) {
|
|
66
|
+
console.warn('⚠ Running inside Claude Code session — Claude invocation tests will be skipped');
|
|
67
|
+
console.warn(' Run outside Claude Code or unset CLAUDECODE to enable full integration tests');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// ─── 1. Pre-flight checks ───
|
|
71
|
+
describe('pre-flight checks (real CLI)', () => {
|
|
72
|
+
it('detects Claude CLI installation', () => {
|
|
73
|
+
const installed = checkClaudeInstalled();
|
|
74
|
+
expect(typeof installed).toBe('boolean');
|
|
75
|
+
});
|
|
76
|
+
it('checks auth status', () => {
|
|
77
|
+
const authed = checkAuth();
|
|
78
|
+
expect(typeof authed).toBe('boolean');
|
|
79
|
+
});
|
|
80
|
+
it('claude --version returns valid output', () => {
|
|
81
|
+
if (!claudeAvailable)
|
|
82
|
+
return;
|
|
83
|
+
const result = spawnSync('claude', ['--version'], { shell: SHELL, encoding: 'utf-8', timeout: 10_000 });
|
|
84
|
+
expect(result.status).toBe(0);
|
|
85
|
+
expect(result.stdout || result.stderr).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
// ─── 2. runClaude — real invocation with simple prompt ───
|
|
89
|
+
// These tests require running OUTSIDE a Claude Code session
|
|
90
|
+
describe('runClaude (real CLI)', () => {
|
|
91
|
+
beforeAll(() => {
|
|
92
|
+
mkdirSync(resolve(TEST_DIR, '.claude'), { recursive: true });
|
|
93
|
+
writeFileSync(resolve(TEST_DIR, '.claude', 'CLAUDE.md'), '# Test Project\nThis is a test.\n');
|
|
94
|
+
});
|
|
95
|
+
it('spawns Claude, gets structured result', async () => {
|
|
96
|
+
if (!claudeAvailable || isNestedSession)
|
|
97
|
+
return;
|
|
98
|
+
const opts = makeOpts({ maxTurns: 2 });
|
|
99
|
+
const output = await runClaude(opts, nopLog, {
|
|
100
|
+
resumePrompt: 'Respond with a structured result. Set status to "completed", action to "test", message to "integration test passed". Do nothing else.',
|
|
101
|
+
});
|
|
102
|
+
expect(output.durationMs).toBeGreaterThan(0);
|
|
103
|
+
expect(output.code).not.toBeNull();
|
|
104
|
+
expect(['none', 'max_turns', 'activity_timeout']).toContain(output.errorCategory);
|
|
105
|
+
}, 120_000);
|
|
106
|
+
it('handles max-turns correctly (1 turn)', async () => {
|
|
107
|
+
if (!claudeAvailable || isNestedSession)
|
|
108
|
+
return;
|
|
109
|
+
const opts = makeOpts({ maxTurns: 1 });
|
|
110
|
+
const output = await runClaude(opts, nopLog, {
|
|
111
|
+
resumePrompt: 'Say hello.',
|
|
112
|
+
});
|
|
113
|
+
expect(['none', 'max_turns']).toContain(output.errorCategory);
|
|
114
|
+
expect(output.durationMs).toBeGreaterThan(0);
|
|
115
|
+
}, 120_000);
|
|
116
|
+
it('captures session ID from stream', async () => {
|
|
117
|
+
if (!claudeAvailable || isNestedSession)
|
|
118
|
+
return;
|
|
119
|
+
const opts = makeOpts({ maxTurns: 1 });
|
|
120
|
+
const output = await runClaude(opts, nopLog, {
|
|
121
|
+
resumePrompt: 'Say "ok".',
|
|
122
|
+
});
|
|
123
|
+
if (output.errorCategory === 'none' || output.errorCategory === 'max_turns') {
|
|
124
|
+
expect(output.sessionId).toBeTruthy();
|
|
125
|
+
}
|
|
126
|
+
}, 120_000);
|
|
127
|
+
it('respects process timeout', async () => {
|
|
128
|
+
if (!claudeAvailable || isNestedSession)
|
|
129
|
+
return;
|
|
130
|
+
const opts = makeOpts({
|
|
131
|
+
maxTurns: 100,
|
|
132
|
+
processTimeout: 5_000,
|
|
133
|
+
});
|
|
134
|
+
const output = await runClaude(opts, nopLog, {
|
|
135
|
+
resumePrompt: 'Write a very long essay about the history of computing. Take your time.',
|
|
136
|
+
});
|
|
137
|
+
expect(output.durationMs).toBeLessThan(30_000);
|
|
138
|
+
expect(['none', 'max_turns', 'process_timeout']).toContain(output.errorCategory);
|
|
139
|
+
}, 60_000);
|
|
140
|
+
it('stream parsing produces display output', async () => {
|
|
141
|
+
if (!claudeAvailable || isNestedSession)
|
|
142
|
+
return;
|
|
143
|
+
const { log, lines } = captureLog();
|
|
144
|
+
const opts = makeOpts({ maxTurns: 1 });
|
|
145
|
+
await runClaude(opts, log, {
|
|
146
|
+
resumePrompt: 'Say "hello integration test".',
|
|
147
|
+
});
|
|
148
|
+
expect(lines.length).toBeGreaterThanOrEqual(0);
|
|
149
|
+
}, 120_000);
|
|
150
|
+
it('detects nested session and returns cli_crash', async () => {
|
|
151
|
+
if (!claudeAvailable || !isNestedSession)
|
|
152
|
+
return;
|
|
153
|
+
const opts = makeOpts({ maxTurns: 1 });
|
|
154
|
+
const output = await runClaude(opts, nopLog, {
|
|
155
|
+
resumePrompt: 'Say hello.',
|
|
156
|
+
});
|
|
157
|
+
// Inside a Claude Code session, Claude refuses to start
|
|
158
|
+
expect(output.errorCategory).toBe('cli_crash');
|
|
159
|
+
expect(output.stderr).toContain('cannot be launched inside another Claude Code session');
|
|
160
|
+
}, 30_000);
|
|
161
|
+
});
|
|
162
|
+
// ─── 3. Structured result parsing ───
|
|
163
|
+
describe('structured result (real CLI)', () => {
|
|
164
|
+
it('parses structured JSON result from Claude', async () => {
|
|
165
|
+
if (!claudeAvailable || isNestedSession)
|
|
166
|
+
return;
|
|
167
|
+
const opts = makeOpts({ maxTurns: 3 });
|
|
168
|
+
const output = await runClaude(opts, nopLog, {
|
|
169
|
+
resumePrompt: 'Return your structured result now. Status: completed. Action: none. Message: "Integration test successful". Do not use any tools. Just return the result immediately.',
|
|
170
|
+
});
|
|
171
|
+
if (output.result) {
|
|
172
|
+
expect(['completed', 'needs_input', 'blocked', 'error']).toContain(output.result.status);
|
|
173
|
+
expect(typeof output.result.action).toBe('string');
|
|
174
|
+
expect(typeof output.result.message).toBe('string');
|
|
175
|
+
}
|
|
176
|
+
expect(output.code).not.toBeNull();
|
|
177
|
+
}, 120_000);
|
|
178
|
+
});
|
|
179
|
+
// ─── 4. Loop continuation — real CLI, two consecutive invocations ───
|
|
180
|
+
// This is the exact scenario that hangs on Windows: first task completes,
|
|
181
|
+
// second invocation should start immediately without getting stuck.
|
|
182
|
+
describe('loop continuation (real CLI, two consecutive runClaude calls)', () => {
|
|
183
|
+
const skipReason = !claudeAvailable ? 'Claude CLI not available' : isNestedSession ? 'nested Claude session' : '';
|
|
184
|
+
it.skipIf(!!skipReason)('second runClaude starts and completes after first one finishes', async () => {
|
|
185
|
+
const opts = makeOpts({ maxTurns: 3 });
|
|
186
|
+
// ── First invocation (simulates iteration 1) ──
|
|
187
|
+
const output1 = await runClaude(opts, nopLog, {
|
|
188
|
+
resumePrompt: 'Return your structured result immediately. Status: completed. Action: commit. Message: "Task T-1 done". task_completed: "T-1". next_task: "T-2". Do not use any tools.',
|
|
189
|
+
});
|
|
190
|
+
expect(output1.code).not.toBeNull();
|
|
191
|
+
expect(['none', 'max_turns']).toContain(output1.errorCategory);
|
|
192
|
+
// ── Second invocation (simulates iteration 2) ──
|
|
193
|
+
// This is where the Windows bug manifests — the second spawn hangs
|
|
194
|
+
// because orphan child processes from the first invocation hold locks.
|
|
195
|
+
const output2 = await runClaude(opts, nopLog, {
|
|
196
|
+
resumePrompt: 'Return your structured result immediately. Status: completed. Action: commit. Message: "Task T-2 done". task_completed: "T-2". next_task: "T-3". Do not use any tools.',
|
|
197
|
+
});
|
|
198
|
+
expect(output2.code).not.toBeNull();
|
|
199
|
+
expect(['none', 'max_turns']).toContain(output2.errorCategory);
|
|
200
|
+
expect(output2.durationMs).toBeGreaterThan(0);
|
|
201
|
+
// Both invocations should have session IDs (proves they actually ran)
|
|
202
|
+
if (output1.errorCategory === 'none')
|
|
203
|
+
expect(output1.sessionId).toBeTruthy();
|
|
204
|
+
if (output2.errorCategory === 'none')
|
|
205
|
+
expect(output2.sessionId).toBeTruthy();
|
|
206
|
+
// Session IDs must differ (proves second is a NEW process, not a stale one)
|
|
207
|
+
if (output1.sessionId && output2.sessionId) {
|
|
208
|
+
expect(output2.sessionId).not.toBe(output1.sessionId);
|
|
209
|
+
}
|
|
210
|
+
}, 180_000); // 3 min timeout — generous for two real Claude calls
|
|
211
|
+
it.skipIf(!!skipReason)('three consecutive invocations all complete (sustained loop)', async () => {
|
|
212
|
+
const opts = makeOpts({ maxTurns: 2 });
|
|
213
|
+
const sessionIds = [];
|
|
214
|
+
for (let i = 1; i <= 3; i++) {
|
|
215
|
+
const output = await runClaude(opts, nopLog, {
|
|
216
|
+
resumePrompt: `Return structured result. Status: completed. Action: done. Message: "Iteration ${i}". Do not use tools.`,
|
|
217
|
+
});
|
|
218
|
+
expect(output.code).not.toBeNull();
|
|
219
|
+
expect(['none', 'max_turns']).toContain(output.errorCategory);
|
|
220
|
+
sessionIds.push(output.sessionId);
|
|
221
|
+
}
|
|
222
|
+
// All sessions should be unique (each is a fresh process)
|
|
223
|
+
const unique = new Set(sessionIds.filter(Boolean));
|
|
224
|
+
expect(unique.size).toBe(sessionIds.filter(Boolean).length);
|
|
225
|
+
}, 300_000); // 5 min for three real calls
|
|
226
|
+
});
|
|
227
|
+
// ─── 5. Scaffolding end-to-end ───
|
|
228
|
+
// (Section 4 is "Loop continuation" above)
|
|
229
|
+
describe('scaffold end-to-end', () => {
|
|
230
|
+
const scaffoldDir = resolve(TEST_DIR, 'scaffold-test');
|
|
231
|
+
it('scaffolds .claude/ directory into target', () => {
|
|
232
|
+
mkdirSync(scaffoldDir, { recursive: true });
|
|
233
|
+
const result = spawnSync('node', [resolve(PROJECT_ROOT, 'dist', 'index.js'), scaffoldDir, '--update'], {
|
|
234
|
+
encoding: 'utf-8',
|
|
235
|
+
timeout: 30_000,
|
|
236
|
+
});
|
|
237
|
+
expect(result.status).toBe(0);
|
|
238
|
+
// Verify key files exist
|
|
239
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'CLAUDE.md'))).toBe(true);
|
|
240
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'agents', 'orchestrator.md'))).toBe(true);
|
|
241
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'agents', 'project-initializer.md'))).toBe(true);
|
|
242
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'scripts', 'autonomous.mjs'))).toBe(true);
|
|
243
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'scripts', 'docker-run.mjs'))).toBe(true);
|
|
244
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'scripts', 'lib', 'claude-runner.mjs'))).toBe(true);
|
|
245
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'templates', 'PLAN.md'))).toBe(true);
|
|
246
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'profiles', 'angular.md'))).toBe(true);
|
|
247
|
+
expect(existsSync(resolve(scaffoldDir, '.claude', 'profiles', 'react.md'))).toBe(true);
|
|
248
|
+
// Verify no spec files leaked into template
|
|
249
|
+
const libDir = resolve(scaffoldDir, '.claude', 'scripts', 'lib');
|
|
250
|
+
if (existsSync(libDir)) {
|
|
251
|
+
const files = execSync(`ls "${libDir}"`, { encoding: 'utf-8' }).trim().split('\n');
|
|
252
|
+
const specFiles = files.filter(f => f.includes('.spec.'));
|
|
253
|
+
expect(specFiles).toEqual([]);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
it('scaffolded autonomous.mjs is valid JavaScript', () => {
|
|
257
|
+
const autonomousPath = resolve(scaffoldDir, '.claude', 'scripts', 'autonomous.mjs');
|
|
258
|
+
if (!existsSync(autonomousPath))
|
|
259
|
+
return;
|
|
260
|
+
const result = spawnSync('node', ['--check', autonomousPath], { encoding: 'utf-8', timeout: 10_000 });
|
|
261
|
+
expect(result.status).toBe(0);
|
|
262
|
+
});
|
|
263
|
+
it('scaffolded scripts have no vitest imports', () => {
|
|
264
|
+
const autonomousPath = resolve(scaffoldDir, '.claude', 'scripts', 'autonomous.mjs');
|
|
265
|
+
if (!existsSync(autonomousPath))
|
|
266
|
+
return;
|
|
267
|
+
const content = readFileSync(autonomousPath, 'utf-8');
|
|
268
|
+
expect(content).not.toContain('vitest');
|
|
269
|
+
expect(content).not.toContain('.spec.');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
// ─── 6. Dry-run validation ───
|
|
273
|
+
describe('dry-run mode', () => {
|
|
274
|
+
it('autonomous --dry-run validates prerequisites without spawning Claude', () => {
|
|
275
|
+
if (!claudeAvailable)
|
|
276
|
+
return;
|
|
277
|
+
const dryRunDir = resolve(TEST_DIR, 'dry-run-test');
|
|
278
|
+
mkdirSync(resolve(dryRunDir, '.claude'), { recursive: true });
|
|
279
|
+
spawnSync('git', ['init'], { cwd: dryRunDir, stdio: 'ignore' });
|
|
280
|
+
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dryRunDir, stdio: 'ignore' });
|
|
281
|
+
spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dryRunDir, stdio: 'ignore' });
|
|
282
|
+
const result = spawnSync('node', [
|
|
283
|
+
resolve(PROJECT_ROOT, 'dist', 'scripts', 'autonomous.mjs'),
|
|
284
|
+
'--dry-run', '--no-pull', '--project-dir', dryRunDir,
|
|
285
|
+
], {
|
|
286
|
+
encoding: 'utf-8',
|
|
287
|
+
timeout: 30_000,
|
|
288
|
+
});
|
|
289
|
+
expect(result.status).toBe(0);
|
|
290
|
+
const output = result.stdout + result.stderr;
|
|
291
|
+
expect(output).toContain('Dry run complete');
|
|
292
|
+
}, 30_000);
|
|
293
|
+
});
|
|
294
|
+
// ─── Cleanup ───
|
|
295
|
+
afterAll(() => {
|
|
296
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
297
|
+
});
|