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
|
@@ -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
|
+
});
|