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.
Files changed (56) hide show
  1. package/README.md +33 -1
  2. package/dist/index.js +29 -56
  3. package/dist/scheduler/agents/health-checker.mjs +98 -0
  4. package/dist/scheduler/agents/health-checker.spec.js +143 -0
  5. package/dist/scheduler/agents/orchestrator.mjs +149 -0
  6. package/dist/scheduler/agents/orchestrator.spec.js +87 -0
  7. package/dist/scheduler/agents/prompt-builder.mjs +204 -0
  8. package/dist/scheduler/agents/prompt-builder.spec.js +240 -0
  9. package/dist/scheduler/agents/worker-pool.mjs +137 -0
  10. package/dist/scheduler/agents/worker-pool.spec.js +45 -0
  11. package/dist/scheduler/git/ci-watcher.mjs +93 -0
  12. package/dist/scheduler/git/ci-watcher.spec.js +35 -0
  13. package/dist/scheduler/git/manager.mjs +228 -0
  14. package/dist/scheduler/git/manager.spec.js +198 -0
  15. package/dist/scheduler/git/release.mjs +117 -0
  16. package/dist/scheduler/git/release.spec.js +175 -0
  17. package/dist/scheduler/index.mjs +309 -0
  18. package/dist/scheduler/index.spec.js +72 -0
  19. package/dist/scheduler/integration.spec.js +289 -0
  20. package/dist/scheduler/loop.mjs +435 -0
  21. package/dist/scheduler/loop.spec.js +139 -0
  22. package/dist/scheduler/state/session.mjs +14 -0
  23. package/dist/scheduler/state/session.spec.js +36 -0
  24. package/dist/scheduler/state/state.mjs +102 -0
  25. package/dist/scheduler/state/state.spec.js +175 -0
  26. package/dist/scheduler/tasks/inbox.mjs +98 -0
  27. package/dist/scheduler/tasks/inbox.spec.js +168 -0
  28. package/dist/scheduler/tasks/parser.mjs +228 -0
  29. package/dist/scheduler/tasks/parser.spec.js +303 -0
  30. package/dist/scheduler/tasks/queue.mjs +152 -0
  31. package/dist/scheduler/tasks/queue.spec.js +223 -0
  32. package/dist/scheduler/types.mjs +20 -0
  33. package/dist/{scripts/lib → scheduler/ui}/tui.mjs +84 -41
  34. package/dist/{scripts/lib → scheduler/ui}/tui.spec.js +56 -0
  35. package/dist/scheduler/util/memory.mjs +126 -0
  36. package/dist/scheduler/util/memory.spec.js +165 -0
  37. package/dist/template/.claude/{profiles/angular.md → agents/angular-engineer.md} +9 -4
  38. package/dist/template/.claude/{profiles/react.md → agents/react-engineer.md} +9 -4
  39. package/dist/template/.claude/{profiles/svelte.md → agents/svelte-engineer.md} +9 -4
  40. package/dist/template/.claude/{profiles/vue.md → agents/vue-engineer.md} +9 -4
  41. package/package.json +3 -4
  42. package/dist/scripts/autonomous.mjs +0 -492
  43. package/dist/scripts/autonomous.spec.js +0 -46
  44. package/dist/scripts/docker-run.mjs +0 -462
  45. package/dist/scripts/integration.spec.js +0 -108
  46. package/dist/scripts/lib/formatter.mjs +0 -309
  47. package/dist/scripts/lib/formatter.spec.js +0 -262
  48. package/dist/scripts/lib/state.mjs +0 -44
  49. package/dist/scripts/lib/state.spec.js +0 -59
  50. package/dist/template/.claude/docker/.dockerignore +0 -8
  51. package/dist/template/.claude/docker/Dockerfile +0 -54
  52. package/dist/template/.claude/docker/docker-compose.yml +0 -22
  53. package/dist/template/.claude/docker/docker-entrypoint.sh +0 -101
  54. /package/dist/{scripts/lib/types.mjs → scheduler/shared-types.mjs} +0 -0
  55. /package/dist/{scripts/lib → scheduler/util}/idle-poll.mjs +0 -0
  56. /package/dist/{scripts/lib → scheduler/util}/idle-poll.spec.js +0 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Integration tests for the scheduler.
3
+ * Tests that use real Claude SDK require ANTHROPIC_API_KEY or OAuth credentials.
4
+ * Skipped automatically when auth is not available.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
8
+ import { resolve, join } from 'node:path';
9
+ import { tmpdir } from 'node:os';
10
+ import { execSync } from 'node:child_process';
11
+ import { WorkerPool } from './agents/worker-pool.mjs';
12
+ import { OrchestratorClient } from './agents/orchestrator.mjs';
13
+ import { checkAuth } from './agents/health-checker.mjs';
14
+ import { parseTodoMd } from './tasks/parser.mjs';
15
+ import { buildGraph, isProjectComplete, isPhaseComplete } from './tasks/queue.mjs';
16
+ import { emptyState, readState, writeState, appendEvent, createEvent } from './state/state.mjs';
17
+ import { recordSession, getSession } from './state/session.mjs';
18
+ import { createRelease } from './git/release.mjs';
19
+ import { createWorktree, commitInWorktree, mergeToMain, cleanupWorktree, getChangedFiles } from './git/manager.mjs';
20
+ const hasAuth = checkAuth();
21
+ const mockLogger = {
22
+ info: () => { },
23
+ warn: () => { },
24
+ error: () => { },
25
+ debug: () => { },
26
+ };
27
+ // ─── Integration: Full pipeline state management ───
28
+ describe('State + Session integration', () => {
29
+ let testDir;
30
+ beforeEach(() => {
31
+ testDir = mkdtempSync(join(tmpdir(), 'sched-integration-'));
32
+ mkdirSync(resolve(testDir, '.claude/scheduler'), { recursive: true });
33
+ });
34
+ afterEach(() => {
35
+ rmSync(testDir, { recursive: true, force: true });
36
+ });
37
+ it('persists state with sessions through write/read cycle', () => {
38
+ const state = emptyState(2);
39
+ state.iteration = 5;
40
+ state.currentPhase = 1;
41
+ state.completedTasks = ['p0-1', 'p0-2'];
42
+ recordSession(state, 'p1-1', 'session-abc');
43
+ recordSession(state, 'p1-2', 'session-def');
44
+ state.pipelines['p1-1'] = {
45
+ taskId: 'p1-1',
46
+ workerId: 0,
47
+ worktreePath: '/tmp/wt1',
48
+ step: 'implement',
49
+ architectPlan: 'Plan for auth',
50
+ apiContract: null,
51
+ reviewFindings: null,
52
+ testingSection: 'Test auth endpoint',
53
+ reviewCycles: 1,
54
+ ciFixes: 0,
55
+ buildFixes: 0,
56
+ assignedAgent: 'backend-ts-architect',
57
+ };
58
+ writeState(testDir, state);
59
+ const loaded = readState(testDir);
60
+ expect(loaded.iteration).toBe(5);
61
+ expect(loaded.completedTasks).toEqual(['p0-1', 'p0-2']);
62
+ expect(getSession(loaded, 'p1-1')).toBe('session-abc');
63
+ expect(loaded.pipelines['p1-1'].step).toBe('implement');
64
+ expect(loaded.pipelines['p1-1'].architectPlan).toBe('Plan for auth');
65
+ });
66
+ it('event log accumulates across multiple appends', () => {
67
+ appendEvent(testDir, createEvent('health_check', { detail: 'start' }));
68
+ appendEvent(testDir, createEvent('task_started', { taskId: 'p0-1' }));
69
+ appendEvent(testDir, createEvent('agent_spawned', { taskId: 'p0-1', agentType: 'backend' }));
70
+ appendEvent(testDir, createEvent('agent_completed', { taskId: 'p0-1' }));
71
+ appendEvent(testDir, createEvent('task_completed', { taskId: 'p0-1' }));
72
+ const logPath = resolve(testDir, '.claude/scheduler/log.ndjson');
73
+ const lines = readFileSync(logPath, 'utf-8').trim().split('\n');
74
+ expect(lines).toHaveLength(5);
75
+ const events = lines.map(l => JSON.parse(l));
76
+ expect(events[0].type).toBe('health_check');
77
+ expect(events[1].taskId).toBe('p0-1');
78
+ expect(events[4].type).toBe('task_completed');
79
+ });
80
+ });
81
+ // ─── Integration: TODO parsing → dependency graph → task queue ───
82
+ describe('TODO → Graph → Queue pipeline', () => {
83
+ it('parses TODO, builds graph, resolves runnable tasks correctly', () => {
84
+ const todo = `# TODO.md
85
+
86
+ ## Phase 0: Foundation
87
+
88
+ - [x] **Init workspace** — scaffolding
89
+ - Type: fullstack
90
+ - Complexity: S
91
+ - Depends on: nothing
92
+
93
+ - [x] **Configure ESLint** — code quality
94
+ - Type: fullstack
95
+ - Complexity: S
96
+ - Depends on: nothing
97
+
98
+ ## Phase 1: Auth
99
+
100
+ - [ ] **Auth types** — interfaces
101
+ - Type: backend
102
+ - Complexity: S
103
+ - Depends on: Phase 0
104
+
105
+ - [ ] **Login API** — endpoint
106
+ - Type: backend
107
+ - Complexity: M
108
+ - Depends on: Auth types
109
+
110
+ - [ ] **Login form** — UI
111
+ - Type: frontend
112
+ - Complexity: S
113
+ - Depends on: Auth types
114
+ `;
115
+ const tasks = parseTodoMd(todo);
116
+ expect(tasks).toHaveLength(5);
117
+ const graph = buildGraph(tasks);
118
+ expect(graph.findCycle()).toBeNull();
119
+ // Phase 0 is complete
120
+ expect(isPhaseComplete(tasks, 0)).toBe(true);
121
+ expect(isPhaseComplete(tasks, 1)).toBe(false);
122
+ // Auth types is runnable (Phase 0 deps are done)
123
+ const runnable = graph.runnable();
124
+ expect(runnable.map(t => t.title)).toContain('Auth types');
125
+ // Login API is blocked (depends on Auth types which is todo)
126
+ expect(runnable.map(t => t.title)).not.toContain('Login API');
127
+ // Login form is also blocked
128
+ expect(runnable.map(t => t.title)).not.toContain('Login form');
129
+ });
130
+ it('handles project completion detection', () => {
131
+ const allDone = `## Phase 0: X
132
+ - [x] **Task A** — done
133
+ - Type: backend
134
+ - Complexity: S
135
+ - [~] **Task B** — skipped
136
+ - Type: backend
137
+ - Complexity: S
138
+ `;
139
+ const tasks = parseTodoMd(allDone);
140
+ expect(isProjectComplete(tasks)).toBe(true);
141
+ });
142
+ });
143
+ // ─── Integration: Git worktree → commit → merge → cleanup ───
144
+ describe('Git worktree lifecycle', () => {
145
+ let repoDir;
146
+ beforeEach(() => {
147
+ repoDir = mkdtempSync(join(tmpdir(), 'git-integration-'));
148
+ execSync('git init -b main', { cwd: repoDir, stdio: 'pipe' });
149
+ execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });
150
+ execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
151
+ writeFileSync(resolve(repoDir, 'README.md'), '# Test');
152
+ execSync('git add . && git commit -m "init"', { cwd: repoDir, stdio: 'pipe' });
153
+ });
154
+ afterEach(() => {
155
+ try {
156
+ const output = execSync('git worktree list --porcelain', { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
157
+ for (const line of output.split('\n')) {
158
+ if (line.startsWith('worktree ') && !line.includes(repoDir.replace(/\\/g, '/'))) {
159
+ const path = line.slice('worktree '.length).trim();
160
+ try {
161
+ execSync(`git worktree remove "${path}" --force`, { cwd: repoDir, stdio: 'pipe' });
162
+ }
163
+ catch { /**/ }
164
+ }
165
+ }
166
+ }
167
+ catch { /**/ }
168
+ rmSync(repoDir, { recursive: true, force: true });
169
+ });
170
+ it('full lifecycle: create worktree → add files → commit → get changes → merge → cleanup', () => {
171
+ // Create
172
+ const wtPath = createWorktree(repoDir, 'feat/p1-1-auth-types');
173
+ expect(existsSync(wtPath)).toBe(true);
174
+ // Add files
175
+ writeFileSync(resolve(wtPath, 'auth.ts'), 'export interface User { id: string; }');
176
+ writeFileSync(resolve(wtPath, 'session.ts'), 'export interface Session { token: string; }');
177
+ // Commit
178
+ const sha = commitInWorktree(wtPath, 'feat: add auth types (#5)');
179
+ expect(sha).toMatch(/^[0-9a-f]{40}$/);
180
+ // Get changes
181
+ const changed = getChangedFiles(wtPath);
182
+ expect(changed).toContain('auth.ts');
183
+ expect(changed).toContain('session.ts');
184
+ // Merge
185
+ const result = mergeToMain(repoDir, 'feat/p1-1-auth-types');
186
+ expect(result.success).toBe(true);
187
+ expect(existsSync(resolve(repoDir, 'auth.ts'))).toBe(true);
188
+ // Cleanup
189
+ cleanupWorktree(repoDir, wtPath, 'feat/p1-1-auth-types');
190
+ expect(existsSync(wtPath)).toBe(false);
191
+ });
192
+ });
193
+ // ─── Integration: Release manager with git ───
194
+ describe('Release manager integration', () => {
195
+ let repoDir;
196
+ beforeEach(() => {
197
+ repoDir = mkdtempSync(join(tmpdir(), 'release-integration-'));
198
+ execSync('git init -b main', { cwd: repoDir, stdio: 'pipe' });
199
+ execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'pipe' });
200
+ execSync('git config user.email "test@test.com"', { cwd: repoDir, stdio: 'pipe' });
201
+ writeFileSync(resolve(repoDir, 'file.txt'), 'init');
202
+ execSync('git add . && git commit -m "init"', { cwd: repoDir, stdio: 'pipe' });
203
+ });
204
+ afterEach(() => {
205
+ rmSync(repoDir, { recursive: true, force: true });
206
+ });
207
+ it('creates release with changelog, correct version bump, and git tag', () => {
208
+ const tasks = [
209
+ { id: 'p1-1', title: 'User auth', phase: 1, type: 'backend', complexity: 'M', status: 'done', dependsOn: [], issueMarker: '#5', kitUpgrade: false, lineNumber: 1, changelog: 'added' },
210
+ { id: 'p1-2', title: 'Fix redirect', phase: 1, type: 'backend', complexity: 'S', status: 'done', dependsOn: [], issueMarker: '#8', kitUpgrade: false, lineNumber: 2, changelog: 'fixed' },
211
+ ];
212
+ const info = createRelease(repoDir, tasks, 1);
213
+ expect(info).not.toBeNull();
214
+ expect(info.version).toBe('v0.1.0');
215
+ // Check changelog
216
+ const changelog = readFileSync(resolve(repoDir, 'CHANGELOG.md'), 'utf-8');
217
+ expect(changelog).toContain('### Added');
218
+ expect(changelog).toContain('User auth (#5)');
219
+ expect(changelog).toContain('### Fixed');
220
+ expect(changelog).toContain('Fix redirect (#8)');
221
+ // Check tag
222
+ const tags = execSync('git tag', { cwd: repoDir, encoding: 'utf-8' }).trim();
223
+ expect(tags).toContain('v0.1.0');
224
+ });
225
+ it('increments from existing tag', () => {
226
+ execSync('git tag -a v1.2.0 -m "v1.2.0"', { cwd: repoDir, stdio: 'pipe' });
227
+ writeFileSync(resolve(repoDir, 'new.txt'), 'x');
228
+ execSync('git add . && git commit -m "change"', { cwd: repoDir, stdio: 'pipe' });
229
+ const tasks = [
230
+ { id: 'p2-1', title: 'Breaking change', phase: 2, type: 'backend', complexity: 'M', status: 'done', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 1, changelog: 'breaking' },
231
+ ];
232
+ const info = createRelease(repoDir, tasks, 2);
233
+ expect(info.version).toBe('v2.0.0');
234
+ expect(info.previousVersion).toBe('v1.2.0');
235
+ });
236
+ });
237
+ // ─── Integration: Real SDK (skipped without auth) ───
238
+ describe.skipIf(!hasAuth)('Real SDK integration', () => {
239
+ it('spawns a Claude process and gets a response', async () => {
240
+ const pool = new WorkerPool({
241
+ concurrency: 1,
242
+ maxTurns: 3,
243
+ skipPermissions: true,
244
+ logger: mockLogger,
245
+ });
246
+ const result = await pool.spawn(0, {
247
+ cwd: process.cwd(),
248
+ prompt: 'Respond with exactly: "SCHEDULER_TEST_OK". Nothing else.',
249
+ model: 'claude-haiku-4-5-20251001',
250
+ });
251
+ expect(result.success).toBe(true);
252
+ expect(result.sessionId).toBeTruthy();
253
+ expect(result.output).toContain('SCHEDULER_TEST_OK');
254
+ expect(result.duration).toBeGreaterThan(0);
255
+ }, 60_000);
256
+ it('orchestrator client routes a task to an agent', async () => {
257
+ const pool = new WorkerPool({
258
+ concurrency: 1,
259
+ maxTurns: 3,
260
+ skipPermissions: true,
261
+ logger: mockLogger,
262
+ });
263
+ const orchestrator = new OrchestratorClient({
264
+ pool,
265
+ projectDir: process.cwd(),
266
+ logger: mockLogger,
267
+ });
268
+ const task = {
269
+ id: 'p1-1',
270
+ title: 'Create REST API endpoint',
271
+ phase: 1,
272
+ type: 'backend',
273
+ complexity: 'M',
274
+ status: 'todo',
275
+ dependsOn: [],
276
+ issueMarker: null,
277
+ kitUpgrade: false,
278
+ lineNumber: 1,
279
+ changelog: 'added',
280
+ };
281
+ const agents = [
282
+ { name: 'backend-ts-architect', description: 'Backend TypeScript specialist', model: 'opus', steps: ['plan', 'implement'], prompt: '' },
283
+ { name: 'ui-engineer', description: 'Frontend specialist', model: 'opus', steps: ['plan', 'implement'], prompt: '' },
284
+ ];
285
+ const decision = await orchestrator.routeTask(task, 'plan', agents);
286
+ expect(decision.agent).toBe('backend-ts-architect');
287
+ expect(decision.reason).toBeTruthy();
288
+ }, 60_000);
289
+ });