tlc-claude-code 2.2.1 → 2.3.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.
@@ -108,6 +108,139 @@ export function removeWorktree(name, { exec } = {}) {
108
108
  exec(`git branch -D ${branch}`);
109
109
  }
110
110
 
111
+ /**
112
+ * Create an integration branch for a phase.
113
+ * All worktrees for this phase branch off and merge back into this branch,
114
+ * producing a single clean PR to main.
115
+ * @param {number} phase - Phase number
116
+ * @param {{ exec: Function, baseBranch?: string }} options
117
+ * @returns {{ branch: string, phase: number }}
118
+ */
119
+ export function createIntegrationBranch(phase, { exec, baseBranch = 'main' } = {}) {
120
+ const branch = `phase/${phase}`;
121
+
122
+ try {
123
+ exec(`git checkout -b ${branch} ${baseBranch}`);
124
+ } catch (err) {
125
+ // Only fall back if branch already exists — surface other errors
126
+ const msg = err && err.message ? err.message : String(err);
127
+ if (msg.includes('already exists')) {
128
+ exec(`git checkout ${branch}`);
129
+ } else {
130
+ throw err;
131
+ }
132
+ }
133
+
134
+ return { branch, phase };
135
+ }
136
+
137
+ /**
138
+ * Get the list of files changed by a branch relative to a base.
139
+ * @param {string} branch - Branch to check
140
+ * @param {string} baseBranch - Base branch to diff against
141
+ * @param {{ exec: Function }} options
142
+ * @returns {string[]} List of changed file paths
143
+ */
144
+ export function getChangedFiles(branch, baseBranch, { exec } = {}) {
145
+ const output = exec(`git diff --name-only ${baseBranch}...${branch}`);
146
+ return output.split('\n').filter(f => f.trim() !== '');
147
+ }
148
+
149
+ /**
150
+ * Merge all worktrees into the integration branch sequentially.
151
+ * After each merge, rebases remaining worktrees onto the updated integration branch.
152
+ * Orders worktrees by file overlap — disjoint worktrees merge first to minimize conflicts.
153
+ * @param {Array<{ name: string, branch: string, path: string }>} worktrees
154
+ * @param {string} integrationBranch
155
+ * @param {{ exec: Function }} options
156
+ * @returns {{ merged: string[], conflicts: string[], integrationBranch: string }}
157
+ */
158
+ export function mergeAllWorktrees(worktrees, integrationBranch, { exec } = {}) {
159
+ const merged = [];
160
+ const conflicts = [];
161
+
162
+ if (worktrees.length === 0) {
163
+ return { merged, conflicts, integrationBranch };
164
+ }
165
+
166
+ // Collect changed files per worktree for overlap analysis
167
+ const fileMap = new Map();
168
+ for (const wt of worktrees) {
169
+ try {
170
+ const files = getChangedFiles(wt.branch, integrationBranch, { exec });
171
+ fileMap.set(wt.name, new Set(files));
172
+ } catch {
173
+ fileMap.set(wt.name, new Set());
174
+ }
175
+ }
176
+
177
+ // Sort: worktrees with fewer file overlaps with others merge first
178
+ const sorted = [...worktrees].sort((a, b) => {
179
+ const aFiles = fileMap.get(a.name) || new Set();
180
+ const bFiles = fileMap.get(b.name) || new Set();
181
+ const allOtherFiles = new Set();
182
+ for (const [name, files] of fileMap) {
183
+ if (name !== a.name && name !== b.name) {
184
+ for (const f of files) allOtherFiles.add(f);
185
+ }
186
+ }
187
+ const aOverlap = [...aFiles].filter(f => allOtherFiles.has(f)).length;
188
+ const bOverlap = [...bFiles].filter(f => allOtherFiles.has(f)).length;
189
+ return aOverlap - bOverlap;
190
+ });
191
+
192
+ for (let i = 0; i < sorted.length; i++) {
193
+ const wt = sorted[i];
194
+ const remaining = sorted.slice(i + 1);
195
+
196
+ // Merge this worktree into the integration branch
197
+ let mergeSuccess = false;
198
+ try {
199
+ exec(`git checkout ${integrationBranch}`);
200
+ } catch (checkoutErr) {
201
+ // Checkout failure is an operational error, not a merge conflict — rethrow
202
+ throw new Error(`Failed to checkout ${integrationBranch}: ${checkoutErr.message || checkoutErr}`);
203
+ }
204
+
205
+ try {
206
+ exec(`git merge ${wt.branch}`);
207
+ mergeSuccess = true;
208
+ } catch (mergeErr) {
209
+ const msg = mergeErr && mergeErr.message ? mergeErr.message : String(mergeErr);
210
+ if (msg.includes('CONFLICT') || msg.includes('conflict') || msg.includes('Merge conflict')) {
211
+ // Content conflict — abort and skip this worktree
212
+ try { exec('git merge --abort'); } catch { /* already clean */ }
213
+ conflicts.push(wt.name);
214
+ } else {
215
+ // Operational error (dirty tree, wrong branch name, etc.) — rethrow
216
+ try { exec('git merge --abort'); } catch { /* best effort */ }
217
+ throw new Error(`Failed to merge ${wt.branch}: ${msg}`);
218
+ }
219
+ }
220
+
221
+ if (mergeSuccess) {
222
+ // Clean up the merged worktree — use --force to handle untracked artifacts
223
+ try { exec(`git worktree remove --force "${wt.path}"`); } catch { /* best effort */ }
224
+ try { exec(`git branch -D ${wt.branch}`); } catch { /* best effort */ }
225
+ merged.push(wt.name);
226
+
227
+ // Rebase remaining worktrees onto the updated integration branch
228
+ // Must use git -C <worktree-path> because the branch is checked out there
229
+ // Quote paths to handle spaces/special characters
230
+ for (const rem of remaining) {
231
+ try {
232
+ exec(`git -C "${rem.path}" rebase ${integrationBranch}`);
233
+ } catch {
234
+ // Rebase conflict — abort and let it be handled when this worktree's turn comes
235
+ try { exec(`git -C "${rem.path}" rebase --abort`); } catch { /* already clean */ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ return { merged, conflicts, integrationBranch };
242
+ }
243
+
111
244
  /**
112
245
  * Remove worktrees whose last commit is older than maxAge milliseconds.
113
246
  * @param {{ exec: Function, maxAge?: number }} options
@@ -6,6 +6,9 @@ import {
6
6
  removeWorktree,
7
7
  cleanupStale,
8
8
  sanitizeName,
9
+ createIntegrationBranch,
10
+ mergeAllWorktrees,
11
+ getChangedFiles,
9
12
  } from './worktree-manager.js';
10
13
 
11
14
  describe('worktree-manager', () => {
@@ -126,4 +129,199 @@ describe('worktree-manager', () => {
126
129
  expect(removed).not.toContain('new-task');
127
130
  });
128
131
  });
132
+
133
+ describe('createIntegrationBranch', () => {
134
+ it('creates branch off base and returns branch name', () => {
135
+ exec.mockReturnValue('');
136
+ const result = createIntegrationBranch(42, { exec });
137
+ expect(result.branch).toBe('phase/42');
138
+ expect(result.phase).toBe(42);
139
+ const cmds = exec.mock.calls.map(c => c[0]);
140
+ expect(cmds.some(c => c.includes('git checkout -b phase/42'))).toBe(true);
141
+ });
142
+
143
+ it('uses custom base branch', () => {
144
+ exec.mockReturnValue('');
145
+ const result = createIntegrationBranch(5, { exec, baseBranch: 'develop' });
146
+ expect(result.branch).toBe('phase/5');
147
+ const cmds = exec.mock.calls.map(c => c[0]);
148
+ expect(cmds.some(c => c.includes('develop'))).toBe(true);
149
+ });
150
+
151
+ it('checks out existing integration branch if it already exists', () => {
152
+ exec.mockImplementation((cmd) => {
153
+ if (cmd.includes('git checkout -b')) throw new Error('fatal: a branch named phase/42 already exists');
154
+ return '';
155
+ });
156
+ const result = createIntegrationBranch(42, { exec });
157
+ expect(result.branch).toBe('phase/42');
158
+ const cmds = exec.mock.calls.map(c => c[0]);
159
+ expect(cmds.some(c => c === 'git checkout phase/42')).toBe(true);
160
+ });
161
+
162
+ it('throws on non-exists errors (e.g., missing base branch)', () => {
163
+ exec.mockImplementation((cmd) => {
164
+ if (cmd.includes('git checkout -b')) throw new Error('fatal: not a valid object name: nonexistent');
165
+ return '';
166
+ });
167
+ expect(() => createIntegrationBranch(42, { exec, baseBranch: 'nonexistent' }))
168
+ .toThrow('not a valid object name');
169
+ });
170
+ });
171
+
172
+ describe('getChangedFiles', () => {
173
+ it('returns list of files changed by a branch', () => {
174
+ exec.mockReturnValue('src/auth.ts\nsrc/user.ts\n');
175
+ const files = getChangedFiles('worktree-task-1', 'phase/42', { exec });
176
+ expect(files).toEqual(['src/auth.ts', 'src/user.ts']);
177
+ });
178
+
179
+ it('returns empty array when no changes', () => {
180
+ exec.mockReturnValue('');
181
+ const files = getChangedFiles('worktree-task-1', 'phase/42', { exec });
182
+ expect(files).toEqual([]);
183
+ });
184
+ });
185
+
186
+ describe('mergeAllWorktrees', () => {
187
+ it('merges worktrees sequentially into integration branch', () => {
188
+ const worktrees = [
189
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
190
+ { name: 'task-2', branch: 'worktree-task-2', path: '/repo/.claude/worktrees/task-2' },
191
+ ];
192
+ exec.mockReturnValue('');
193
+
194
+ const result = mergeAllWorktrees(worktrees, 'phase/42', { exec });
195
+
196
+ expect(result.merged).toEqual(['task-1', 'task-2']);
197
+ expect(result.conflicts).toEqual([]);
198
+ expect(result.integrationBranch).toBe('phase/42');
199
+ });
200
+
201
+ it('rebases remaining worktrees after each merge', () => {
202
+ const worktrees = [
203
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
204
+ { name: 'task-2', branch: 'worktree-task-2', path: '/repo/.claude/worktrees/task-2' },
205
+ { name: 'task-3', branch: 'worktree-task-3', path: '/repo/.claude/worktrees/task-3' },
206
+ ];
207
+ exec.mockReturnValue('');
208
+
209
+ mergeAllWorktrees(worktrees, 'phase/42', { exec });
210
+
211
+ // After merging task-1, should rebase task-2 and task-3 using git -C <path>
212
+ const cmds = exec.mock.calls.map(c => c[0]);
213
+ const rebaseCmds = cmds.filter(c => c.includes('rebase'));
214
+ expect(rebaseCmds.length).toBeGreaterThanOrEqual(2);
215
+ // Must use git -C to rebase within the worktree (branch is checked out there)
216
+ expect(rebaseCmds.every(c => c.includes('git -C'))).toBe(true);
217
+ });
218
+
219
+ it('continues merging others when one worktree conflicts', () => {
220
+ const worktrees = [
221
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
222
+ { name: 'task-2', branch: 'worktree-task-2', path: '/repo/.claude/worktrees/task-2' },
223
+ { name: 'task-3', branch: 'worktree-task-3', path: '/repo/.claude/worktrees/task-3' },
224
+ ];
225
+
226
+ exec.mockImplementation((cmd) => {
227
+ // task-2 merge conflicts
228
+ if (cmd.includes('git merge worktree-task-2')) throw new Error('CONFLICT');
229
+ // task-2 rebase also fails (expected after conflict)
230
+ if (cmd.includes('git rebase') && cmd.includes('worktree-task-2')) throw new Error('CONFLICT');
231
+ return '';
232
+ });
233
+
234
+ const result = mergeAllWorktrees(worktrees, 'phase/42', { exec });
235
+
236
+ expect(result.merged).toContain('task-1');
237
+ expect(result.merged).toContain('task-3');
238
+ expect(result.conflicts).toContain('task-2');
239
+ });
240
+
241
+ it('cleans up worktrees after successful merge', () => {
242
+ const worktrees = [
243
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
244
+ ];
245
+ exec.mockReturnValue('');
246
+
247
+ mergeAllWorktrees(worktrees, 'phase/42', { exec });
248
+
249
+ const cmds = exec.mock.calls.map(c => c[0]);
250
+ expect(cmds.some(c => c.includes('git worktree remove'))).toBe(true);
251
+ expect(cmds.some(c => c.includes('git branch -D worktree-task-1'))).toBe(true);
252
+ });
253
+
254
+ it('preserves conflicting worktrees for manual resolution', () => {
255
+ const worktrees = [
256
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
257
+ ];
258
+
259
+ exec.mockImplementation((cmd) => {
260
+ if (cmd.includes('git merge')) throw new Error('CONFLICT');
261
+ return '';
262
+ });
263
+
264
+ mergeAllWorktrees(worktrees, 'phase/42', { exec });
265
+
266
+ const cmds = exec.mock.calls.map(c => c[0]);
267
+ expect(cmds.some(c => c.includes('git worktree remove'))).toBe(false);
268
+ });
269
+
270
+ it('throws on non-conflict merge errors (dirty tree, wrong branch)', () => {
271
+ const worktrees = [
272
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
273
+ ];
274
+
275
+ exec.mockImplementation((cmd) => {
276
+ if (cmd.includes('git merge')) throw new Error('fatal: not something we can merge');
277
+ return '';
278
+ });
279
+
280
+ expect(() => mergeAllWorktrees(worktrees, 'phase/42', { exec }))
281
+ .toThrow('Failed to merge');
282
+ });
283
+
284
+ it('orders worktrees by file overlap — disjoint first', () => {
285
+ const worktrees = [
286
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
287
+ { name: 'task-2', branch: 'worktree-task-2', path: '/repo/.claude/worktrees/task-2' },
288
+ { name: 'task-3', branch: 'worktree-task-3', path: '/repo/.claude/worktrees/task-3' },
289
+ ];
290
+
291
+ exec.mockImplementation((cmd) => {
292
+ // task-1 and task-3 touch the same file, task-2 is disjoint
293
+ if (cmd.includes('diff --name-only') && cmd.includes('worktree-task-1')) return 'src/shared.ts\nsrc/a.ts\n';
294
+ if (cmd.includes('diff --name-only') && cmd.includes('worktree-task-2')) return 'src/b.ts\n';
295
+ if (cmd.includes('diff --name-only') && cmd.includes('worktree-task-3')) return 'src/shared.ts\nsrc/c.ts\n';
296
+ return '';
297
+ });
298
+
299
+ const result = mergeAllWorktrees(worktrees, 'phase/42', { exec });
300
+
301
+ // task-2 (disjoint) should merge before task-1 or task-3 (overlapping)
302
+ const mergeOrder = result.merged;
303
+ const task2Index = mergeOrder.indexOf('task-2');
304
+ expect(task2Index).toBeLessThan(mergeOrder.length - 1);
305
+ });
306
+
307
+ it('returns empty result for empty worktree list', () => {
308
+ const result = mergeAllWorktrees([], 'phase/42', { exec });
309
+ expect(result.merged).toEqual([]);
310
+ expect(result.conflicts).toEqual([]);
311
+ });
312
+
313
+ it('handles single worktree without rebase', () => {
314
+ const worktrees = [
315
+ { name: 'task-1', branch: 'worktree-task-1', path: '/repo/.claude/worktrees/task-1' },
316
+ ];
317
+ exec.mockReturnValue('');
318
+
319
+ const result = mergeAllWorktrees(worktrees, 'phase/42', { exec });
320
+
321
+ expect(result.merged).toEqual(['task-1']);
322
+ const cmds = exec.mock.calls.map(c => c[0]);
323
+ // No rebase needed for a single worktree
324
+ expect(cmds.some(c => c.includes('git rebase'))).toBe(false);
325
+ });
326
+ });
129
327
  });
@@ -10,7 +10,7 @@
10
10
  * Use --agents N to limit parallelism to specific number
11
11
  *
12
12
  * Opus 4.6 Multi-Agent Features:
13
- * - Model selection per agent (opus/sonnet/haiku) based on task complexity
13
+ * - Model selection per agent (always opus for quality)
14
14
  * - Agent resumption via `resume` parameter for retry/continuation
15
15
  * - TaskOutput for non-blocking progress checks on background agents
16
16
  * - TaskStop for cancelling stuck agents
@@ -32,13 +32,13 @@ const AGENT_TYPES = {
32
32
  };
33
33
 
34
34
  /**
35
- * Model tiers for cost/capability optimization (Opus 4.6)
36
- * Agents are assigned models based on task complexity.
35
+ * Model tiers (Opus 4.6)
36
+ * All tasks use opus — cheaper models produce lower quality that costs more in fix cycles.
37
37
  */
38
38
  const MODEL_TIERS = {
39
- HEAVY: 'opus', // Complex multi-file features, architectural work
40
- STANDARD: 'sonnet', // Normal implementation tasks (default)
41
- LIGHT: 'haiku', // Simple tasks: config, boilerplate, single-file changes
39
+ HEAVY: 'opus',
40
+ STANDARD: 'opus',
41
+ LIGHT: 'opus',
42
42
  };
43
43
 
44
44
  /**
@@ -79,7 +79,7 @@ function estimateTaskComplexity(task) {
79
79
  * Get model for a task based on its complexity
80
80
  * @param {Object} task - Task object
81
81
  * @param {string} [modelOverride] - Force a specific model
82
- * @returns {string} Model name (opus, sonnet, haiku)
82
+ * @returns {string} Model name (always opus)
83
83
  */
84
84
  function getModelForTask(task, modelOverride) {
85
85
  if (modelOverride) {
@@ -127,7 +127,7 @@ function parseOverdriveArgs(args = '') {
127
127
  options.mode = parts[++i];
128
128
  } else if (part === '--model' && parts[i + 1]) {
129
129
  const model = parts[++i].toLowerCase();
130
- if (['opus', 'sonnet', 'haiku'].includes(model)) {
130
+ if (['opus'].includes(model)) {
131
131
  options.model = model;
132
132
  }
133
133
  } else if (part === '--max-turns' && parts[i + 1]) {
@@ -305,8 +305,15 @@ function formatOverdrivePlan(plan) {
305
305
  lines.push('All agents spawned simultaneously via Task tool (Opus 4.6 multi-agent).');
306
306
  lines.push('Each agent works independently until completion.');
307
307
  lines.push('');
308
+ lines.push('**Integration Branch:**');
309
+ lines.push(`- All worktrees branch off \`phase/${plan.phase}\` (not main)`);
310
+ lines.push('- After all agents complete, worktrees merge sequentially into the integration branch');
311
+ lines.push('- Disjoint worktrees merge first, overlapping ones last');
312
+ lines.push('- Remaining worktrees rebase onto integration branch after each merge');
313
+ lines.push('- Single PR from integration branch to main');
314
+ lines.push('');
308
315
  lines.push('**Capabilities:**');
309
- lines.push('- Model selection per task complexity (opus/sonnet/haiku)');
316
+ lines.push('- All agents use opus for maximum quality');
310
317
  lines.push('- Agent resumption for failed tasks (resume parameter)');
311
318
  lines.push('- Non-blocking progress checks (TaskOutput block=false)');
312
319
  lines.push('- Agent cancellation (TaskStop) for stuck agents');
@@ -448,12 +455,21 @@ async function executeOverdriveCommand(args = '', context = {}) {
448
455
  const allPrompts = agentAssignments.flatMap(a => a.prompts);
449
456
  const taskCalls = generateTaskCalls(allPrompts);
450
457
 
458
+ // Integration branch name for this phase
459
+ const integrationBranch = `phase/${options.phase}`;
460
+
451
461
  return {
452
462
  success: true,
453
463
  plan,
454
464
  taskCalls,
465
+ integrationBranch,
455
466
  output: formatOverdrivePlan(plan),
456
467
  instructions: `
468
+ BEFORE SPAWNING AGENTS:
469
+ 1. Create integration branch: git checkout -b ${integrationBranch} main
470
+ (If it already exists: git checkout ${integrationBranch})
471
+ 2. All worktrees will branch from ${integrationBranch}, not main.
472
+
457
473
  EXECUTE NOW: Spawn ${agentCount} agents in parallel using the Task tool (Opus 4.6).
458
474
 
459
475
  ${taskCalls.map((tc, i) => `
@@ -471,6 +487,12 @@ Task(
471
487
  CRITICAL: Call ALL Task tools in a SINGLE message to run them in parallel.
472
488
  Do NOT wait between spawns. Fire them all at once.
473
489
 
490
+ AFTER ALL AGENTS COMPLETE:
491
+ 1. List worktrees for THIS phase only (filter by "phase-${options.phase}-task")
492
+ 2. Merge all worktrees into ${integrationBranch} using mergeAllWorktrees()
493
+ 3. Run full test suite on ${integrationBranch}
494
+ 4. Create single PR from ${integrationBranch} to main
495
+
474
496
  MONITORING: Use TaskOutput(task_id, block=false) to check progress.
475
497
  STUCK AGENT: Use TaskStop(task_id) to cancel, then resume with Task(resume=agent_id).
476
498
  FAILED AGENT: Use Task(resume=agent_id) to continue from where it left off.
@@ -75,11 +75,11 @@ describe('overdrive-command', () => {
75
75
 
76
76
  it('parses --model flag', () => {
77
77
  expect(parseOverdriveArgs('--model opus').model).toBe('opus');
78
- expect(parseOverdriveArgs('--model sonnet').model).toBe('sonnet');
79
- expect(parseOverdriveArgs('--model haiku').model).toBe('haiku');
80
78
  });
81
79
 
82
- it('ignores invalid model values', () => {
80
+ it('ignores non-opus model values', () => {
81
+ expect(parseOverdriveArgs('--model sonnet').model).toBeNull();
82
+ expect(parseOverdriveArgs('--model haiku').model).toBeNull();
83
83
  expect(parseOverdriveArgs('--model gpt4').model).toBeNull();
84
84
  });
85
85
 
@@ -89,12 +89,12 @@ describe('overdrive-command', () => {
89
89
  });
90
90
 
91
91
  it('parses multiple flags including new ones', () => {
92
- const options = parseOverdriveArgs('5 --agents 2 --mode test --model sonnet --max-turns 25 --dry-run');
92
+ const options = parseOverdriveArgs('5 --agents 2 --mode test --model opus --max-turns 25 --dry-run');
93
93
 
94
94
  expect(options.phase).toBe(5);
95
95
  expect(options.agents).toBe(2);
96
96
  expect(options.mode).toBe('test');
97
- expect(options.model).toBe('sonnet');
97
+ expect(options.model).toBe('opus');
98
98
  expect(options.maxTurns).toBe(25);
99
99
  expect(options.dryRun).toBe(true);
100
100
  });
@@ -152,7 +152,7 @@ describe('overdrive-command', () => {
152
152
  expect(prompts[0].prompt).toContain('Fix any failing tests');
153
153
  });
154
154
 
155
- it('assigns model based on task complexity', () => {
155
+ it('assigns opus for all task complexities', () => {
156
156
  const tasks = [
157
157
  { id: 1, title: 'Refactor authentication system' },
158
158
  { id: 2, title: 'Add helper function' },
@@ -163,9 +163,9 @@ describe('overdrive-command', () => {
163
163
  mode: 'build', projectDir: '/p', phase: 1,
164
164
  });
165
165
 
166
- expect(prompts[0].model).toBe('opus'); // refactor = heavy
167
- expect(prompts[1].model).toBe('sonnet'); // default = standard
168
- expect(prompts[2].model).toBe('haiku'); // enum = light
166
+ expect(prompts[0].model).toBe('opus');
167
+ expect(prompts[1].model).toBe('opus');
168
+ expect(prompts[2].model).toBe('opus');
169
169
  });
170
170
 
171
171
  it('respects model override', () => {
@@ -176,11 +176,11 @@ describe('overdrive-command', () => {
176
176
 
177
177
  const prompts = generateAgentPrompts(tasks, {
178
178
  mode: 'build', projectDir: '/p', phase: 1,
179
- model: 'haiku',
179
+ model: 'opus',
180
180
  });
181
181
 
182
- expect(prompts[0].model).toBe('haiku');
183
- expect(prompts[1].model).toBe('haiku');
182
+ expect(prompts[0].model).toBe('opus');
183
+ expect(prompts[1].model).toBe('opus');
184
184
  });
185
185
 
186
186
  it('respects maxTurns override', () => {
@@ -286,8 +286,8 @@ describe('overdrive-command', () => {
286
286
  describe('generateTaskCalls', () => {
287
287
  it('generates task tool calls with model and max_turns', () => {
288
288
  const prompts = [
289
- { taskId: 1, taskTitle: 'Test', prompt: 'Do task 1', agentType: 'general-purpose', model: 'sonnet', maxTurns: 50 },
290
- { taskId: 2, taskTitle: 'Test 2', prompt: 'Do task 2', agentType: 'general-purpose', model: 'haiku', maxTurns: 30 },
289
+ { taskId: 1, taskTitle: 'Test', prompt: 'Do task 1', agentType: 'general-purpose', model: 'opus', maxTurns: 50 },
290
+ { taskId: 2, taskTitle: 'Test 2', prompt: 'Do task 2', agentType: 'general-purpose', model: 'opus', maxTurns: 30 },
291
291
  ];
292
292
 
293
293
  const calls = generateTaskCalls(prompts);
@@ -297,9 +297,9 @@ describe('overdrive-command', () => {
297
297
  expect(calls[0].params.description).toContain('Agent 1');
298
298
  expect(calls[0].params.run_in_background).toBe(true);
299
299
  expect(calls[0].params.subagent_type).toBe('general-purpose');
300
- expect(calls[0].params.model).toBe('sonnet');
300
+ expect(calls[0].params.model).toBe('opus');
301
301
  expect(calls[0].params.max_turns).toBe(50);
302
- expect(calls[1].params.model).toBe('haiku');
302
+ expect(calls[1].params.model).toBe('opus');
303
303
  expect(calls[1].params.max_turns).toBe(30);
304
304
  });
305
305
  });
@@ -497,16 +497,16 @@ Blocked by Task 1
497
497
  expect(getModelForTask({ title: 'Refactor auth' })).toBe('opus');
498
498
  });
499
499
 
500
- it('returns sonnet for standard tasks', () => {
501
- expect(getModelForTask({ title: 'Add endpoint' })).toBe('sonnet');
500
+ it('returns opus for standard tasks', () => {
501
+ expect(getModelForTask({ title: 'Add endpoint' })).toBe('opus');
502
502
  });
503
503
 
504
- it('returns haiku for light tasks', () => {
505
- expect(getModelForTask({ title: 'Create enum' })).toBe('haiku');
504
+ it('returns opus for light tasks', () => {
505
+ expect(getModelForTask({ title: 'Create enum' })).toBe('opus');
506
506
  });
507
507
 
508
508
  it('respects model override', () => {
509
- expect(getModelForTask({ title: 'Refactor auth' }, 'haiku')).toBe('haiku');
509
+ expect(getModelForTask({ title: 'Refactor auth' }, 'opus')).toBe('opus');
510
510
  expect(getModelForTask({ title: 'Create enum' }, 'opus')).toBe('opus');
511
511
  });
512
512
  });
@@ -533,10 +533,10 @@ Blocked by Task 1
533
533
  expect(AGENT_TYPES.PLAN).toBe('Plan');
534
534
  });
535
535
 
536
- it('exports valid model tiers', () => {
536
+ it('exports valid model tiers (all opus)', () => {
537
537
  expect(MODEL_TIERS.HEAVY).toBe('opus');
538
- expect(MODEL_TIERS.STANDARD).toBe('sonnet');
539
- expect(MODEL_TIERS.LIGHT).toBe('haiku');
538
+ expect(MODEL_TIERS.STANDARD).toBe('opus');
539
+ expect(MODEL_TIERS.LIGHT).toBe('opus');
540
540
  });
541
541
 
542
542
  it('exports default max turns', () => {
@@ -562,8 +562,7 @@ Blocked by Task 1
562
562
 
563
563
  expect(output).toContain('Opus 4.6');
564
564
  expect(output).toContain('[opus]');
565
- expect(output).toContain('[haiku]');
566
- expect(output).toContain('Model selection per task complexity');
565
+ expect(output).toContain('All agents use opus');
567
566
  expect(output).toContain('Agent resumption');
568
567
  expect(output).toContain('TaskOutput');
569
568
  expect(output).toContain('TaskStop');