create-claude-workspace 1.1.45 → 1.1.46

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.
Files changed (26) hide show
  1. package/dist/index.js +30 -7
  2. package/dist/{template/.claude/scripts → scripts}/autonomous.mjs +0 -1
  3. package/dist/scripts/autonomous.spec.js +36 -0
  4. package/dist/{template/.claude/scripts → scripts}/docker-run.mjs +4 -6
  5. package/dist/scripts/integration.spec.js +297 -0
  6. package/dist/scripts/lib/claude-runner.spec.js +238 -0
  7. package/dist/scripts/lib/errors.spec.js +192 -0
  8. package/dist/scripts/lib/logger.spec.js +56 -0
  9. package/dist/scripts/lib/loop-continuation.integration.spec.js +205 -0
  10. package/dist/scripts/lib/loop-integration.spec.js +798 -0
  11. package/dist/scripts/lib/oauth-refresh.spec.js +126 -0
  12. package/dist/scripts/lib/state.spec.js +88 -0
  13. package/dist/scripts/lib/utils.spec.js +184 -0
  14. package/dist/template/.claude/CLAUDE.md +4 -1
  15. package/dist/template/.claude/agents/backend-ts-architect.md +15 -10
  16. package/dist/template/.claude/agents/orchestrator.md +33 -2
  17. package/dist/template/.claude/agents/ui-engineer.md +1 -1
  18. package/dist/template/.claude/templates/claude-md.md +51 -22
  19. package/package.json +2 -1
  20. /package/dist/{template/.claude/scripts → scripts}/lib/claude-runner.mjs +0 -0
  21. /package/dist/{template/.claude/scripts → scripts}/lib/errors.mjs +0 -0
  22. /package/dist/{template/.claude/scripts → scripts}/lib/logger.mjs +0 -0
  23. /package/dist/{template/.claude/scripts → scripts}/lib/oauth-refresh.mjs +0 -0
  24. /package/dist/{template/.claude/scripts → scripts}/lib/state.mjs +0 -0
  25. /package/dist/{template/.claude/scripts → scripts}/lib/types.mjs +0 -0
  26. /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
- results.push(...listFiles(full, base));
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(relative(base, full));
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
- const script = docker ? '.claude/scripts/docker-run.mjs' : '.claude/scripts/autonomous.mjs';
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
- const files = listFiles(TEMPLATE_DIR);
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.`);
@@ -131,7 +131,6 @@ function printHelp() {
131
131
  Autonomous development loop for Claude Code.
132
132
 
133
133
  Usage: npx create-claude-workspace run [options]
134
- node .claude/scripts/autonomous.mjs [options]
135
134
 
136
135
  Options:
137
136
  --max-iterations <n> Max iterations (default: 50)
@@ -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, resolve, dirname } from 'node:path';
6
+ import { join } from 'node:path';
7
7
  import { homedir, platform as osPlatform, tmpdir } from 'node:os';
8
- import { fileURLToPath } from 'node:url';
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
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', `node .claude/scripts/autonomous.mjs ${escaped.join(' ')}`]);
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
+ });