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,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractJson, parseRoutingDecision, parseFailureDecision, parseMergeConflictDecision, } from './orchestrator.mjs';
3
+ // ─── extractJson ───
4
+ describe('extractJson', () => {
5
+ it('extracts JSON from code block', () => {
6
+ const output = 'Some text\n```json\n{"agent": "backend"}\n```\nMore text';
7
+ expect(extractJson(output)).toBe('{"agent": "backend"}');
8
+ });
9
+ it('extracts JSON from code block without language tag', () => {
10
+ const output = '```\n{"action": "retry"}\n```';
11
+ expect(extractJson(output)).toBe('{"action": "retry"}');
12
+ });
13
+ it('extracts raw JSON object', () => {
14
+ const output = 'I think {"agent": "ui-engineer", "reason": "frontend task"} is the best choice.';
15
+ expect(extractJson(output)).toBe('{"agent": "ui-engineer", "reason": "frontend task"}');
16
+ });
17
+ it('returns null for no JSON', () => {
18
+ expect(extractJson('Just plain text with no JSON')).toBeNull();
19
+ });
20
+ });
21
+ // ─── parseRoutingDecision ───
22
+ describe('parseRoutingDecision', () => {
23
+ it('parses valid routing decision', () => {
24
+ const output = '```json\n{"agent": "backend-ts-architect", "reason": "Backend task"}\n```';
25
+ const decision = parseRoutingDecision(output);
26
+ expect(decision.agent).toBe('backend-ts-architect');
27
+ expect(decision.reason).toBe('Backend task');
28
+ });
29
+ it('parses create decision', () => {
30
+ const output = '```json\n{"agent": null, "reason": "No DB specialist", "create": {"name": "db-specialist", "description": "DB expert", "model": "opus", "steps": ["plan"], "prompt": "You are a DB expert"}}\n```';
31
+ const decision = parseRoutingDecision(output);
32
+ expect(decision.agent).toBeNull();
33
+ expect(decision.create).toBeDefined();
34
+ expect(decision.create.name).toBe('db-specialist');
35
+ });
36
+ it('returns fallback for unparseable output', () => {
37
+ const decision = parseRoutingDecision('random text');
38
+ expect(decision.agent).toBeNull();
39
+ expect(decision.reason).toContain('Failed to parse');
40
+ });
41
+ it('returns fallback for invalid JSON', () => {
42
+ const decision = parseRoutingDecision('```json\n{broken json\n```');
43
+ expect(decision.agent).toBeNull();
44
+ expect(decision.reason).toContain('Invalid JSON');
45
+ });
46
+ });
47
+ // ─── parseFailureDecision ───
48
+ describe('parseFailureDecision', () => {
49
+ it('parses retry decision', () => {
50
+ const output = '```json\n{"action": "retry", "reason": "Transient error", "guidance": "Try with a different approach"}\n```';
51
+ const decision = parseFailureDecision(output);
52
+ expect(decision.action).toBe('retry');
53
+ expect(decision.reason).toBe('Transient error');
54
+ expect(decision.guidance).toBe('Try with a different approach');
55
+ });
56
+ it('parses skip decision', () => {
57
+ const output = '{"action": "skip", "reason": "Dependency not available"}';
58
+ const decision = parseFailureDecision(output);
59
+ expect(decision.action).toBe('skip');
60
+ });
61
+ it('parses escalate decision', () => {
62
+ const output = '```json\n{"action": "escalate", "reason": "Needs human review"}\n```';
63
+ const decision = parseFailureDecision(output);
64
+ expect(decision.action).toBe('escalate');
65
+ });
66
+ it('defaults to skip on parse failure', () => {
67
+ const decision = parseFailureDecision('no json here');
68
+ expect(decision.action).toBe('skip');
69
+ });
70
+ });
71
+ // ─── parseMergeConflictDecision ───
72
+ describe('parseMergeConflictDecision', () => {
73
+ it('parses rebase decision', () => {
74
+ const output = '```json\n{"action": "rebase", "reason": "Simple conflict, rebase will work"}\n```';
75
+ const decision = parseMergeConflictDecision(output);
76
+ expect(decision.action).toBe('rebase');
77
+ });
78
+ it('parses resolve decision', () => {
79
+ const output = '{"action": "resolve", "reason": "Complex conflict in shared code"}';
80
+ const decision = parseMergeConflictDecision(output);
81
+ expect(decision.action).toBe('resolve');
82
+ });
83
+ it('defaults to skip on parse failure', () => {
84
+ const decision = parseMergeConflictDecision('unparseable');
85
+ expect(decision.action).toBe('skip');
86
+ });
87
+ });
@@ -0,0 +1,204 @@
1
+ // ─── Minimal prompt construction for each pipeline step ───
2
+ // No workflow instructions. Just task + context + expected output format.
3
+ // ─── Planning prompts ───
4
+ export function buildPlanPrompt(ctx) {
5
+ const lines = [
6
+ `Plan the implementation for the following task.`,
7
+ ``,
8
+ `## Task`,
9
+ `**${ctx.task.title}** (${ctx.task.type}, Complexity: ${ctx.task.complexity})`,
10
+ ``,
11
+ `## Working Directory`,
12
+ ctx.worktreePath,
13
+ ``,
14
+ `## Project Root`,
15
+ ctx.projectDir,
16
+ ];
17
+ if (ctx.apiContract) {
18
+ lines.push(``, `## API Contract (from backend architect)`, ctx.apiContract);
19
+ }
20
+ if (ctx.profileContent) {
21
+ lines.push(``, `## Frontend Profile`, ctx.profileContent);
22
+ }
23
+ lines.push(``, `## Instructions`, `Read the codebase at the working directory. Produce a structured plan.`, ``, `## Required Output Format`, ``, `### EXISTING CODE`, `List relevant existing files, functions, and patterns that can be reused.`, ``, `### SCOPE`, `What needs to change — be specific about additions vs modifications.`, ``, `### FILES`, `List every file to create or modify, with the intended change.`, ``, `### IMPLEMENTATION ORDER`, `Numbered sequence of steps.`, ``, `### INTERFACES`, `TypeScript interfaces/types to define or modify.`, ``, `### REUSE OPPORTUNITIES`, `Existing code that can be leveraged to avoid duplication.`, ``, `### PATTERNS`, `Architectural patterns to follow (from existing codebase).`, ``, `### PITFALLS`, `Known risks or edge cases.`, ``, `### TESTING`, `What to test, what to skip, and why. This section will be forwarded to the test engineer.`, ``, `### SPLIT RECOMMENDATION`, `If this task is too large for a single implementation pass, recommend how to split it. Otherwise write "No split needed."`);
24
+ return lines.join('\n');
25
+ }
26
+ // ─── Implementation prompt ───
27
+ export function buildImplementPrompt(ctx) {
28
+ const lines = [
29
+ `Implement the following task according to the architect's plan.`,
30
+ ``,
31
+ `## Task`,
32
+ `**${ctx.task.title}** (${ctx.task.type}, Complexity: ${ctx.task.complexity})`,
33
+ ``,
34
+ `## Working Directory`,
35
+ ctx.worktreePath,
36
+ ];
37
+ if (ctx.architectPlan) {
38
+ lines.push(``, `## Architect's Plan`, ctx.architectPlan);
39
+ }
40
+ if (ctx.apiContract) {
41
+ lines.push(``, `## API Contract`, ctx.apiContract);
42
+ }
43
+ if (ctx.profileContent) {
44
+ lines.push(``, `## Frontend Profile`, ctx.profileContent);
45
+ }
46
+ lines.push(``, `## Instructions`, `Follow the plan exactly. Write production code. Run build/lint to verify.`, `If build or lint fails, fix the issues before finishing.`);
47
+ return lines.join('\n');
48
+ }
49
+ // ─── Test prompt ───
50
+ export function buildTestPrompt(ctx) {
51
+ const lines = [
52
+ `Write and run tests for the following task.`,
53
+ ``,
54
+ `## Task`,
55
+ `**${ctx.task.title}** (${ctx.task.type})`,
56
+ ``,
57
+ `## Working Directory`,
58
+ ctx.worktreePath,
59
+ ];
60
+ if (ctx.changedFiles && ctx.changedFiles.length > 0) {
61
+ lines.push(``, `## Changed Files`, ...ctx.changedFiles.map(f => `- ${f}`));
62
+ }
63
+ if (ctx.testingSection) {
64
+ lines.push(``, `## Architect's Testing Decisions`, ctx.testingSection);
65
+ }
66
+ lines.push(``, `## Instructions`, `- Auto-detect test framework (Vitest or Jest) from project config`, `- Write unit tests for pure logic and business rules`, `- Write integration tests for API endpoints and DB operations (*.integration.spec.ts)`, `- Co-locate test files next to source (foo.spec.ts beside foo.ts)`, `- Run tests with --coverage and ensure 80% threshold`, `- If E2E tests are appropriate for this task, write Playwright tests`);
67
+ return lines.join('\n');
68
+ }
69
+ // ─── Review prompt ───
70
+ export function buildReviewPrompt(ctx) {
71
+ const lines = [
72
+ `Review the code changes for the following task.`,
73
+ ``,
74
+ `## Task`,
75
+ `**${ctx.task.title}** (${ctx.task.type})`,
76
+ ``,
77
+ `## Working Directory`,
78
+ ctx.worktreePath,
79
+ ];
80
+ if (ctx.changedFiles && ctx.changedFiles.length > 0) {
81
+ lines.push(``, `## Changed Files`, ...ctx.changedFiles.map(f => `- ${f}`));
82
+ }
83
+ if (ctx.testingSection) {
84
+ lines.push(``, `## Architect's Testing Decisions`, ctx.testingSection);
85
+ }
86
+ lines.push(``, `## Instructions`, `Review all changed files. Use this structured output format:`, ``, `### CRITICAL`, `Bugs, security issues, data loss risks. Must be fixed before merge.`, ``, `### WARN`, `Code quality issues, potential problems. Should be fixed.`, ``, `### NICE-TO-HAVE`, `Improvements that would be good but are not blocking.`, ``, `### GREEN`, `What was done well.`, ``, `End with a verdict: **PASS** (no CRITICAL/WARN) or **FAIL** (has CRITICAL/WARN items).`);
87
+ return lines.join('\n');
88
+ }
89
+ // ─── Rework prompt ───
90
+ export function buildReworkPrompt(ctx) {
91
+ const lines = [
92
+ `Fix the review findings for the following task.`,
93
+ ``,
94
+ `## Task`,
95
+ `**${ctx.task.title}**`,
96
+ ``,
97
+ `## Working Directory`,
98
+ ctx.worktreePath,
99
+ ];
100
+ if (ctx.reviewFindings) {
101
+ lines.push(``, `## Review Findings`, ctx.reviewFindings);
102
+ }
103
+ lines.push(``, `## Instructions`, `Fix all CRITICAL and WARN items. Only change what is flagged.`, `Run build/lint/tests after changes to verify nothing broke.`);
104
+ return lines.join('\n');
105
+ }
106
+ // ─── Decomposition prompt (for L-tasks) ───
107
+ export function buildDecomposePrompt(task) {
108
+ return [
109
+ `This task is Complexity: L and should be split into smaller tasks.`,
110
+ ``,
111
+ `## Task`,
112
+ `**${task.title}** (${task.type}, Phase ${task.phase})`,
113
+ ``,
114
+ `## Instructions`,
115
+ `Split this task into 2-4 smaller tasks (Complexity S or M).`,
116
+ `Each sub-task must be independently implementable and testable.`,
117
+ ``,
118
+ `## Required Output Format`,
119
+ `Return a JSON array:`,
120
+ '```json',
121
+ `[`,
122
+ ` { "title": "Sub-task title", "type": "${task.type}", "complexity": "S", "dependsOn": [] },`,
123
+ ` { "title": "Next sub-task", "type": "${task.type}", "complexity": "M", "dependsOn": ["Sub-task title"] }`,
124
+ `]`,
125
+ '```',
126
+ ].join('\n');
127
+ }
128
+ // ─── Routing prompt (orchestrator decides which agent handles a task) ───
129
+ export function buildRoutingPrompt(task, step, agents) {
130
+ const agentList = agents
131
+ .map(a => `- **${a.name}**: ${a.description} (steps: ${a.steps.join(', ')})`)
132
+ .join('\n');
133
+ return [
134
+ `Select the best agent for this task and step.`,
135
+ ``,
136
+ `## Task`,
137
+ `**${task.title}** (${task.type}, Complexity: ${task.complexity})`,
138
+ ``,
139
+ `## Pipeline Step`,
140
+ step,
141
+ ``,
142
+ `## Available Agents`,
143
+ agentList,
144
+ ``,
145
+ `## Instructions`,
146
+ `Choose the most qualified agent. If no agent fits, respond with agent: null and provide a new agent definition.`,
147
+ ``,
148
+ `## Required Output Format`,
149
+ `Respond with JSON only:`,
150
+ '```json',
151
+ `{ "agent": "agent-name", "reason": "why this agent" }`,
152
+ '```',
153
+ `Or if a new agent is needed:`,
154
+ '```json',
155
+ `{ "agent": null, "reason": "why no existing agent fits", "create": { "name": "new-agent", "description": "...", "model": "opus", "steps": ["plan", "implement"], "prompt": "You are a..." } }`,
156
+ '```',
157
+ ].join('\n');
158
+ }
159
+ // ─── CI fix prompt ───
160
+ export function buildCIFixPrompt(ctx, ciLogs) {
161
+ return [
162
+ `The CI pipeline failed. Diagnose and fix the issue.`,
163
+ ``,
164
+ `## Task`,
165
+ `**${ctx.task.title}**`,
166
+ ``,
167
+ `## Working Directory`,
168
+ ctx.worktreePath,
169
+ ``,
170
+ `## CI Failure Logs`,
171
+ '```',
172
+ ciLogs,
173
+ '```',
174
+ ``,
175
+ `## Instructions`,
176
+ `- Analyze the logs to find the root cause`,
177
+ `- Fix the issue in the code`,
178
+ `- Run build/lint/tests locally to verify the fix`,
179
+ ].join('\n');
180
+ }
181
+ // ─── Phase transition prompt (for product-owner re-evaluation) ───
182
+ export function buildPhaseTransitionPrompt(completedPhase, nextPhase) {
183
+ return [
184
+ `Phase ${completedPhase} is complete. Evaluate whether to proceed to Phase ${nextPhase}.`,
185
+ ``,
186
+ `## Instructions`,
187
+ `Review PRODUCT.md and the completed tasks. Respond with a structured evaluation:`,
188
+ ``,
189
+ `### CONFIRM`,
190
+ `Tasks in Phase ${nextPhase} that should proceed as planned.`,
191
+ ``,
192
+ `### REPRIORITIZE`,
193
+ `Tasks that should change priority order.`,
194
+ ``,
195
+ `### ADD`,
196
+ `New tasks discovered during Phase ${completedPhase} that should be added.`,
197
+ ``,
198
+ `### REMOVE`,
199
+ `Tasks that are no longer needed.`,
200
+ ``,
201
+ `### VERDICT`,
202
+ `PROCEED (continue to Phase ${nextPhase}) or REVISE (update TODO.md first).`,
203
+ ].join('\n');
204
+ }
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildPlanPrompt, buildImplementPrompt, buildTestPrompt, buildReviewPrompt, buildReworkPrompt, buildDecomposePrompt, buildRoutingPrompt, buildCIFixPrompt, buildPhaseTransitionPrompt, } from './prompt-builder.mjs';
3
+ // ─── Fixtures ───
4
+ const backendTask = {
5
+ id: 'p1-1',
6
+ title: 'Create auth API endpoint',
7
+ phase: 1,
8
+ type: 'backend',
9
+ complexity: 'M',
10
+ status: 'todo',
11
+ dependsOn: [],
12
+ issueMarker: '#5',
13
+ kitUpgrade: false,
14
+ lineNumber: 10,
15
+ changelog: 'added',
16
+ };
17
+ const frontendTask = {
18
+ id: 'p1-2',
19
+ title: 'User registration form',
20
+ phase: 1,
21
+ type: 'frontend',
22
+ complexity: 'S',
23
+ status: 'todo',
24
+ dependsOn: ['p1-1'],
25
+ issueMarker: null,
26
+ kitUpgrade: false,
27
+ lineNumber: 20,
28
+ changelog: 'added',
29
+ };
30
+ const largeTask = {
31
+ id: 'p2-1',
32
+ title: 'Full dashboard with analytics',
33
+ phase: 2,
34
+ type: 'fullstack',
35
+ complexity: 'L',
36
+ status: 'todo',
37
+ dependsOn: [],
38
+ issueMarker: null,
39
+ kitUpgrade: false,
40
+ lineNumber: 30,
41
+ changelog: 'added',
42
+ };
43
+ const baseCtx = {
44
+ task: backendTask,
45
+ worktreePath: '/project/.worktrees/feat/5-auth',
46
+ projectDir: '/project',
47
+ };
48
+ const agents = [
49
+ { name: 'backend-ts-architect', description: 'Backend specialist', model: 'opus', steps: ['plan', 'implement', 'rework'], prompt: '' },
50
+ { name: 'ui-engineer', description: 'Frontend specialist', model: 'opus', steps: ['plan', 'implement', 'rework'], prompt: '' },
51
+ { name: 'test-engineer', description: 'Test specialist', model: 'sonnet', steps: ['test'], prompt: '' },
52
+ ];
53
+ // ─── Plan prompt ───
54
+ describe('buildPlanPrompt', () => {
55
+ it('includes task title and metadata', () => {
56
+ const prompt = buildPlanPrompt(baseCtx);
57
+ expect(prompt).toContain('Create auth API endpoint');
58
+ expect(prompt).toContain('backend');
59
+ expect(prompt).toContain('Complexity: M');
60
+ });
61
+ it('includes working directory', () => {
62
+ const prompt = buildPlanPrompt(baseCtx);
63
+ expect(prompt).toContain('/project/.worktrees/feat/5-auth');
64
+ });
65
+ it('includes required output sections', () => {
66
+ const prompt = buildPlanPrompt(baseCtx);
67
+ expect(prompt).toContain('### EXISTING CODE');
68
+ expect(prompt).toContain('### SCOPE');
69
+ expect(prompt).toContain('### FILES');
70
+ expect(prompt).toContain('### IMPLEMENTATION ORDER');
71
+ expect(prompt).toContain('### TESTING');
72
+ expect(prompt).toContain('### SPLIT RECOMMENDATION');
73
+ });
74
+ it('includes API contract when provided', () => {
75
+ const ctx = { ...baseCtx, apiContract: 'POST /users → 201' };
76
+ const prompt = buildPlanPrompt(ctx);
77
+ expect(prompt).toContain('API Contract');
78
+ expect(prompt).toContain('POST /users → 201');
79
+ });
80
+ it('includes frontend profile when provided', () => {
81
+ const ctx = { ...baseCtx, task: frontendTask, profileContent: 'Use React hooks' };
82
+ const prompt = buildPlanPrompt(ctx);
83
+ expect(prompt).toContain('Frontend Profile');
84
+ expect(prompt).toContain('Use React hooks');
85
+ });
86
+ it('omits API contract section when not provided', () => {
87
+ const prompt = buildPlanPrompt(baseCtx);
88
+ expect(prompt).not.toContain('API Contract');
89
+ });
90
+ it('does not contain workflow instructions', () => {
91
+ const prompt = buildPlanPrompt(baseCtx);
92
+ expect(prompt).not.toContain('STEP 1');
93
+ expect(prompt).not.toContain('STEP 2');
94
+ // "worktree" appears in path which is fine, but not as workflow instruction
95
+ expect(prompt).not.toContain('create worktree');
96
+ expect(prompt.toLowerCase()).not.toContain('commit');
97
+ expect(prompt.toLowerCase()).not.toContain('merge');
98
+ });
99
+ });
100
+ // ─── Implement prompt ───
101
+ describe('buildImplementPrompt', () => {
102
+ it('includes task and plan', () => {
103
+ const ctx = { ...baseCtx, architectPlan: 'Create users.ts with POST handler' };
104
+ const prompt = buildImplementPrompt(ctx);
105
+ expect(prompt).toContain('Create auth API endpoint');
106
+ expect(prompt).toContain("Architect's Plan");
107
+ expect(prompt).toContain('Create users.ts with POST handler');
108
+ });
109
+ it('instructs to run build/lint', () => {
110
+ const prompt = buildImplementPrompt(baseCtx);
111
+ expect(prompt).toContain('build/lint');
112
+ });
113
+ it('does not contain workflow instructions', () => {
114
+ const prompt = buildImplementPrompt(baseCtx);
115
+ expect(prompt).not.toContain('STEP');
116
+ expect(prompt.toLowerCase()).not.toContain('commit');
117
+ });
118
+ });
119
+ // ─── Test prompt ───
120
+ describe('buildTestPrompt', () => {
121
+ it('includes changed files', () => {
122
+ const ctx = { ...baseCtx, changedFiles: ['src/routes/users.ts', 'src/models/user.ts'] };
123
+ const prompt = buildTestPrompt(ctx);
124
+ expect(prompt).toContain('src/routes/users.ts');
125
+ expect(prompt).toContain('src/models/user.ts');
126
+ });
127
+ it('includes testing section from architect', () => {
128
+ const ctx = { ...baseCtx, testingSection: 'Skip E2E, focus on unit tests' };
129
+ const prompt = buildTestPrompt(ctx);
130
+ expect(prompt).toContain("Architect's Testing Decisions");
131
+ expect(prompt).toContain('Skip E2E, focus on unit tests');
132
+ });
133
+ it('mentions coverage threshold', () => {
134
+ const prompt = buildTestPrompt(baseCtx);
135
+ expect(prompt).toContain('80%');
136
+ });
137
+ it('mentions co-located test files', () => {
138
+ const prompt = buildTestPrompt(baseCtx);
139
+ expect(prompt).toContain('Co-locate');
140
+ });
141
+ });
142
+ // ─── Review prompt ───
143
+ describe('buildReviewPrompt', () => {
144
+ it('includes structured output sections', () => {
145
+ const prompt = buildReviewPrompt(baseCtx);
146
+ expect(prompt).toContain('### CRITICAL');
147
+ expect(prompt).toContain('### WARN');
148
+ expect(prompt).toContain('### NICE-TO-HAVE');
149
+ expect(prompt).toContain('### GREEN');
150
+ expect(prompt).toContain('**PASS**');
151
+ expect(prompt).toContain('**FAIL**');
152
+ });
153
+ it('includes changed files when provided', () => {
154
+ const ctx = { ...baseCtx, changedFiles: ['src/foo.ts'] };
155
+ const prompt = buildReviewPrompt(ctx);
156
+ expect(prompt).toContain('src/foo.ts');
157
+ });
158
+ });
159
+ // ─── Rework prompt ───
160
+ describe('buildReworkPrompt', () => {
161
+ it('includes review findings', () => {
162
+ const ctx = { ...baseCtx, reviewFindings: 'CRITICAL: SQL injection in query builder' };
163
+ const prompt = buildReworkPrompt(ctx);
164
+ expect(prompt).toContain('SQL injection in query builder');
165
+ });
166
+ it('instructs to fix only flagged items', () => {
167
+ const prompt = buildReworkPrompt(baseCtx);
168
+ expect(prompt).toContain('Only change what is flagged');
169
+ });
170
+ });
171
+ // ─── Decompose prompt ───
172
+ describe('buildDecomposePrompt', () => {
173
+ it('references the task details', () => {
174
+ const prompt = buildDecomposePrompt(largeTask);
175
+ expect(prompt).toContain('Full dashboard with analytics');
176
+ expect(prompt).toContain('fullstack');
177
+ expect(prompt).toContain('Phase 2');
178
+ });
179
+ it('requests JSON output', () => {
180
+ const prompt = buildDecomposePrompt(largeTask);
181
+ expect(prompt).toContain('JSON array');
182
+ expect(prompt).toContain('"title"');
183
+ expect(prompt).toContain('"complexity"');
184
+ });
185
+ it('mentions Complexity L', () => {
186
+ const prompt = buildDecomposePrompt(largeTask);
187
+ expect(prompt).toContain('Complexity: L');
188
+ });
189
+ });
190
+ // ─── Routing prompt ───
191
+ describe('buildRoutingPrompt', () => {
192
+ it('lists all available agents', () => {
193
+ const prompt = buildRoutingPrompt(backendTask, 'plan', agents);
194
+ expect(prompt).toContain('backend-ts-architect');
195
+ expect(prompt).toContain('ui-engineer');
196
+ expect(prompt).toContain('test-engineer');
197
+ });
198
+ it('includes task details', () => {
199
+ const prompt = buildRoutingPrompt(backendTask, 'plan', agents);
200
+ expect(prompt).toContain('Create auth API endpoint');
201
+ expect(prompt).toContain('backend');
202
+ });
203
+ it('includes the pipeline step', () => {
204
+ const prompt = buildRoutingPrompt(backendTask, 'implement', agents);
205
+ expect(prompt).toContain('implement');
206
+ });
207
+ it('requests JSON output with create option', () => {
208
+ const prompt = buildRoutingPrompt(backendTask, 'plan', agents);
209
+ expect(prompt).toContain('"agent"');
210
+ expect(prompt).toContain('"create"');
211
+ });
212
+ });
213
+ // ─── CI fix prompt ───
214
+ describe('buildCIFixPrompt', () => {
215
+ it('includes CI logs', () => {
216
+ const prompt = buildCIFixPrompt(baseCtx, 'Error: test failed at line 42');
217
+ expect(prompt).toContain('Error: test failed at line 42');
218
+ });
219
+ it('instructs to analyze and fix', () => {
220
+ const prompt = buildCIFixPrompt(baseCtx, 'logs');
221
+ expect(prompt).toContain('root cause');
222
+ expect(prompt).toContain('Fix the issue');
223
+ });
224
+ });
225
+ // ─── Phase transition prompt ───
226
+ describe('buildPhaseTransitionPrompt', () => {
227
+ it('references completed and next phases', () => {
228
+ const prompt = buildPhaseTransitionPrompt(1, 2);
229
+ expect(prompt).toContain('Phase 1');
230
+ expect(prompt).toContain('Phase 2');
231
+ });
232
+ it('includes evaluation sections', () => {
233
+ const prompt = buildPhaseTransitionPrompt(0, 1);
234
+ expect(prompt).toContain('### CONFIRM');
235
+ expect(prompt).toContain('### REPRIORITIZE');
236
+ expect(prompt).toContain('### ADD');
237
+ expect(prompt).toContain('### REMOVE');
238
+ expect(prompt).toContain('### VERDICT');
239
+ });
240
+ });
@@ -0,0 +1,137 @@
1
+ // ─── Manage N concurrent Claude processes via Agent SDK ───
2
+ import { query } from '@anthropic-ai/claude-agent-sdk';
3
+ import { emptyWorker } from '../state/state.mjs';
4
+ // Disable SDK built-in agents
5
+ process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS = '1';
6
+ export class WorkerPool {
7
+ workers;
8
+ queries = new Map();
9
+ opts;
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ this.workers = Array.from({ length: opts.concurrency }, (_, i) => emptyWorker(i));
13
+ }
14
+ get slots() {
15
+ return this.workers;
16
+ }
17
+ activeCount() {
18
+ return this.workers.filter(w => w.status === 'running').length;
19
+ }
20
+ idleSlot() {
21
+ return this.workers.find(w => w.status === 'idle') ?? null;
22
+ }
23
+ getSlot(id) {
24
+ return this.workers[id] ?? null;
25
+ }
26
+ async spawn(slotId, opts) {
27
+ const slot = this.workers[slotId];
28
+ if (!slot)
29
+ throw new Error(`Invalid worker slot: ${slotId}`);
30
+ if (slot.status === 'running')
31
+ throw new Error(`Worker ${slotId} is already running`);
32
+ const startTime = Date.now();
33
+ slot.status = 'running';
34
+ slot.agentType = opts.agent ?? null;
35
+ slot.startedAt = startTime;
36
+ const queryOptions = {
37
+ settingSources: ['project'],
38
+ maxTurns: this.opts.maxTurns,
39
+ cwd: opts.cwd,
40
+ };
41
+ if (opts.agent)
42
+ queryOptions.agent = opts.agent;
43
+ if (opts.model)
44
+ queryOptions.model = opts.model;
45
+ if (opts.resume) {
46
+ queryOptions.resume = opts.resume;
47
+ }
48
+ if (this.opts.skipPermissions) {
49
+ queryOptions.permissionMode = 'bypassPermissions';
50
+ queryOptions.allowDangerouslySkipPermissions = true;
51
+ }
52
+ let output = '';
53
+ let sessionId = '';
54
+ let success = true;
55
+ let error;
56
+ let cost = 0;
57
+ try {
58
+ const q = query({
59
+ prompt: opts.prompt,
60
+ options: queryOptions,
61
+ });
62
+ this.queries.set(slotId, q);
63
+ for await (const message of q) {
64
+ // Track session ID
65
+ if (message.type === 'system' && message.subtype === 'init') {
66
+ sessionId = message.session_id || '';
67
+ slot.sessionId = sessionId;
68
+ }
69
+ // Collect text output
70
+ if (message.type === 'assistant') {
71
+ const content = message.message?.content;
72
+ if (Array.isArray(content)) {
73
+ for (const block of content) {
74
+ if (block.type === 'text')
75
+ output += block.text;
76
+ }
77
+ }
78
+ }
79
+ // Track result
80
+ if (message.type === 'result') {
81
+ sessionId = message.session_id ?? sessionId;
82
+ slot.sessionId = sessionId;
83
+ if ('is_error' in message && message.is_error) {
84
+ success = false;
85
+ error = 'errors' in message ? message.errors?.join('; ') : 'Agent failed';
86
+ }
87
+ if ('total_cost_usd' in message) {
88
+ cost = message.total_cost_usd ?? 0;
89
+ }
90
+ }
91
+ // Forward to caller
92
+ opts.onMessage?.(message);
93
+ }
94
+ }
95
+ catch (err) {
96
+ success = false;
97
+ error = err.message;
98
+ this.opts.logger.error(`Worker ${slotId} error: ${error}`);
99
+ }
100
+ finally {
101
+ this.queries.delete(slotId);
102
+ slot.status = 'idle';
103
+ slot.taskId = null;
104
+ slot.pid = null;
105
+ slot.startedAt = null;
106
+ slot.agentType = null;
107
+ slot.step = null;
108
+ }
109
+ return {
110
+ success,
111
+ sessionId,
112
+ output,
113
+ error,
114
+ duration: Date.now() - startTime,
115
+ cost,
116
+ };
117
+ }
118
+ async killAll() {
119
+ for (const [slotId, q] of this.queries) {
120
+ try {
121
+ await q.return(undefined);
122
+ }
123
+ catch { /* ignore */ }
124
+ const slot = this.workers[slotId];
125
+ slot.status = 'idle';
126
+ slot.taskId = null;
127
+ slot.pid = null;
128
+ }
129
+ this.queries.clear();
130
+ }
131
+ reset() {
132
+ for (let i = 0; i < this.workers.length; i++) {
133
+ this.workers[i] = emptyWorker(i);
134
+ }
135
+ this.queries.clear();
136
+ }
137
+ }