From 53a473da4fb194f1dca2582f158fe64208528a55 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 19 Sep 2026 17:12:02 -0700 Subject: [PATCH 1/2] feat(flows): offer Grok in onboarding --- web/app/flows/onboarding/WorkflowPicker.tsx | 4 ++-- web/lib/flow-agent-settings.ts | 8 +++---- web/lib/flow-agents.ts | 4 ++-- web/lib/test/flow-agent-settings.test.ts | 26 +++++++++++++-------- web/lib/test/flow-analytics.test.ts | 4 ++-- web/lib/test/flow-onboarding.test.ts | 12 +++++----- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/web/app/flows/onboarding/WorkflowPicker.tsx b/web/app/flows/onboarding/WorkflowPicker.tsx index b956199..463545f 100644 --- a/web/app/flows/onboarding/WorkflowPicker.tsx +++ b/web/app/flows/onboarding/WorkflowPicker.tsx @@ -6,7 +6,7 @@ import { WORKFLOWS, WORKFLOW_STEP_DETAILS, workflowAgents } from '../../../lib/f import { SiGithub, SiGitlab } from 'react-icons/si'; import Claude from '@lobehub/icons/es/Claude'; import Codex from '@lobehub/icons/es/Codex'; -import Cursor from '@lobehub/icons/es/Cursor'; +import Grok from '@lobehub/icons/es/Grok'; import OpenCode from '@lobehub/icons/es/OpenCode'; import { agentLabel, canContinue, isCodingAgent, type CodingAgent, type FactoryDraft } from '../../../lib/flow-onboarding'; import { repositoryHost, sourceLabel, sourceSummary } from '../../../lib/flow-sources'; @@ -15,7 +15,7 @@ import type { FlowTrack } from '../../../lib/flow-analytics'; import s from './onboarding.module.css'; function ProcessAgent({ id }: { id: CodingAgent }) { - const Icon = { claude: Claude.Color, codex: Codex.Color, cursor: Cursor, opencode: OpenCode }[id]; + const Icon = { claude: Claude.Color, codex: Codex.Color, opencode: OpenCode, grok: Grok }[id]; return ; } diff --git a/web/lib/flow-agent-settings.ts b/web/lib/flow-agent-settings.ts index 1a038b1..94b27bf 100644 --- a/web/lib/flow-agent-settings.ts +++ b/web/lib/flow-agent-settings.ts @@ -1,9 +1,9 @@ -import { isCodingAgent, type CodingAgent } from './flow-agents'; +import { CODING_AGENTS, isCodingAgent, type AgentId, type CodingAgent } from './flow-agents'; import type { WorkflowId, WorkflowStep } from './flow-workflows'; export const AGENT_ROLES = ['planner', 'plan-reviewer', 'prototype-1', 'prototype-2', 'prototype-3', 'comparator', 'implementer', 'adversary', 'fixer', 'check-discovery', 'check-repair'] as const; export type AgentRole = typeof AGENT_ROLES[number]; -export type AgentSettings = { agent?: CodingAgent; model?: string; prompt?: string }; +export type AgentSettings = { agent?: AgentId; model?: string; prompt?: string }; export type FlowAgentSettings = Partial>; export function rolesForStep(step: WorkflowStep): AgentRole[] { @@ -36,7 +36,7 @@ export function resolveAgentSettings(workflow: WorkflowId, role: AgentRole, sele const defaultAgent = ['plan-reviewer', 'comparator', 'adversary', 'prototype-2'].includes(role) ? reviewer : builder; const saved = settings[`${workflow}:${role}`]; // Changing the selected agents must never leave an unavailable CLI assigned. - const agent = saved?.agent && available.includes(saved.agent) ? saved.agent : defaultAgent; + const agent = saved?.agent && isCodingAgent(saved.agent) && available.includes(saved.agent) ? saved.agent : defaultAgent; const compatible = !saved?.agent || saved.agent === agent; return { agent, model: compatible ? saved?.model?.trim() || '' : '', prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) }; } @@ -48,7 +48,7 @@ export function validFlowAgentSettings(value: unknown): value is FlowAgentSettin const [workflow, role, extra] = key.split(':'); if (extra || !['traditional', 'prototype', 'simple'].includes(workflow) || !(AGENT_ROLES as readonly string[]).includes(role)) return false; if (!settings || typeof settings !== 'object' || Array.isArray(settings)) return false; - return Object.entries(settings).every(([field, v]) => field === 'agent' ? typeof v === 'string' && isCodingAgent(v) + return Object.entries(settings).every(([field, v]) => field === 'agent' ? typeof v === 'string' && CODING_AGENTS.some(agent => agent.id === v) : field === 'model' ? typeof v === 'string' && v.length <= 120 && !/[\r\n\0]/.test(v) : field === 'prompt' ? typeof v === 'string' && v.trim().length > 0 && v.length <= 6000 : false); }); diff --git a/web/lib/flow-agents.ts b/web/lib/flow-agents.ts index de84c5a..cce56da 100644 --- a/web/lib/flow-agents.ts +++ b/web/lib/flow-agents.ts @@ -3,12 +3,12 @@ export const CODING_AGENTS = [ { id: 'codex', label: 'Codex', available: true }, { id: 'gemini', label: 'Gemini CLI', available: false }, { id: 'opencode', label: 'OpenCode', available: true }, - { id: 'cursor', label: 'Cursor', available: true }, + { id: 'cursor', label: 'Cursor', available: false }, { id: 'copilot', label: 'GitHub Copilot', available: false }, { id: 'windsurf', label: 'Windsurf', available: false }, { id: 'aider', label: 'Aider', available: false }, { id: 'goose', label: 'Goose', available: false }, - { id: 'grok', label: 'Grok', available: false }, + { id: 'grok', label: 'Grok', available: true }, { id: 'pi', label: 'Pi', available: false }, ] as const; export type AgentId = (typeof CODING_AGENTS)[number]['id']; diff --git a/web/lib/test/flow-agent-settings.test.ts b/web/lib/test/flow-agent-settings.test.ts index 2cc7f56..624d3f2 100644 --- a/web/lib/test/flow-agent-settings.test.ts +++ b/web/lib/test/flow-agent-settings.test.ts @@ -4,7 +4,7 @@ import { DEFAULT_FACTORY, factorySource, readFactoryDraft, cloudConnectionsHref, import { resolveAgentSettings } from '../flow-agent-settings'; import { localKitFiles } from '../flow-local'; -const draft: FactoryDraft = { ...DEFAULT_FACTORY, sources: ['github'], agents: ['claude', 'codex', 'cursor', 'opencode'], workflow: 'prototype', step: 3 }; +const draft: FactoryDraft = { ...DEFAULT_FACTORY, sources: ['github'], agents: ['claude', 'codex', 'grok', 'opencode'], workflow: 'prototype', step: 3 }; async function execute(value: FactoryDraft) { const source = factorySource(value).replace('import { flow } from "@relayflows/surface";', ''); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }); @@ -22,21 +22,27 @@ async function execute(value: FactoryDraft) { describe('per-step agent settings', () => { it('inherits CLI models and adapts assignments to the selected agents', () => { - expect(resolveAgentSettings('traditional', 'planner', ['cursor', 'opencode'])).toMatchObject({ agent: 'cursor', model: '' }); - expect(resolveAgentSettings('traditional', 'adversary', ['cursor', 'opencode'])).toMatchObject({ agent: 'opencode', model: '' }); + expect(resolveAgentSettings('traditional', 'planner', ['grok', 'opencode'])).toMatchObject({ agent: 'grok', model: '' }); + expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'opencode'])).toMatchObject({ agent: 'opencode', model: '' }); expect(resolveAgentSettings('prototype', 'prototype-2', ['codex']).agent).toBe('codex'); - expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'cursor', model: 'cursor-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); + expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); + }); + + it('keeps old Cursor settings readable while falling back to a supported agent', () => { + const saved = { 'simple:implementer': { agent: 'cursor' as const, model: 'cursor-model', prompt: 'Custom work' } }; + expect(readFactoryDraft(JSON.stringify({ ...draft, agentSettings: saved })))?.toMatchObject({ agentSettings: saved }); + expect(resolveAgentSettings('simple', 'implementer', ['cursor'], saved)).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); }); it('runs distinct prototype overrides while preserving ticket and worktree context', async () => { const prompt = 'Use "quotes", `ticks`, ${literal}, and a newline.\nWrite prototype-notes.md.'; const calls = await execute({ ...draft, task: 'Keep changes focused.', agentSettings: { - 'prototype:prototype-1': { agent: 'cursor', model: 'model-one', prompt }, + 'prototype:prototype-1': { agent: 'grok', model: 'model-one', prompt }, 'prototype:prototype-2': { agent: 'opencode', model: 'model-two' }, 'prototype:comparator': { agent: 'codex', prompt: 'Write comparison.md.' }, 'prototype:implementer': { agent: 'opencode', model: 'build-model' }, } }); - expect(calls['prototype-1']).toMatchObject({ cli: 'cursor', model: 'model-one', cwd: '/tmp/prototypes/1' }); + expect(calls['prototype-1']).toMatchObject({ cli: 'grok', model: 'model-one', cwd: '/tmp/prototypes/1' }); expect(calls['prototype-1'].task).toContain(prompt); expect(calls['prototype-1'].task).toContain('Ticket title\nTicket body\nKeep changes focused.'); expect(calls['prototype-1'].task).toContain('Assigned approach: the smallest change'); @@ -47,16 +53,16 @@ describe('per-step agent settings', () => { }); it('applies the shared reviewer settings to both traditional rounds', async () => { - const calls = await execute({ ...draft, workflow: 'traditional', agentSettings: { 'traditional:adversary': { agent: 'cursor', model: 'review-model', prompt: 'Check the diff. Write review.clean only if clean.' } } }); - for (const role of ['adversary-1', 'adversary-2']) expect(calls[role]).toMatchObject({ cli: 'cursor', model: 'review-model' }); + const calls = await execute({ ...draft, workflow: 'traditional', agentSettings: { 'traditional:adversary': { agent: 'grok', model: 'review-model', prompt: 'Check the diff. Write review.clean only if clean.' } } }); + for (const role of ['adversary-1', 'adversary-2']) expect(calls[role]).toMatchObject({ cli: 'grok', model: 'review-model' }); expect(calls.planner.model).toBeUndefined(); }); it('persists valid overrides and includes them in both handoff sources', () => { - const value: FactoryDraft = { ...draft, agentSettings: { 'prototype:implementer': { agent: 'cursor', model: 'custom-model', prompt: 'Write summary.md.' } } }; + const value: FactoryDraft = { ...draft, agentSettings: { 'prototype:implementer': { agent: 'grok', model: 'custom-model', prompt: 'Write summary.md.' } } }; expect(readFactoryDraft(JSON.stringify(value))?.agentSettings).toEqual(value.agentSettings); expect(localKitFiles(value)['software-factory.flow.mts']).toContain('model: "custom-model"'); - expect(localKitFiles(value)['START-HERE.txt']).toContain('Cursor'); + expect(localKitFiles(value)['START-HERE.txt']).toContain('Grok'); const cloud = JSON.parse(decodeURIComponent(new URL(cloudConnectionsHref(value, 'test')).hash.slice(1))); expect(cloud.source).toContain('model: "custom-model"'); expect(cloud.source).toContain('Write summary.md.'); diff --git a/web/lib/test/flow-analytics.test.ts b/web/lib/test/flow-analytics.test.ts index 5d81d22..4d8636e 100644 --- a/web/lib/test/flow-analytics.test.ts +++ b/web/lib/test/flow-analytics.test.ts @@ -12,8 +12,8 @@ function fixture() { } describe('onboarding funnel accounting', () => { - it('counts Cursor and OpenCode as supported agents', () => { - expect(flowMetrics({ ...DEFAULT_FACTORY, agents: ['cursor', 'opencode', 'pi'] })).toMatchObject({ + it('counts Grok and OpenCode as supported agents', () => { + expect(flowMetrics({ ...DEFAULT_FACTORY, agents: ['grok', 'opencode', 'cursor'] })).toMatchObject({ supported_agent_count: 2, requested_agent_count: 1, }); }); diff --git a/web/lib/test/flow-onboarding.test.ts b/web/lib/test/flow-onboarding.test.ts index 6a8be58..5bd0810 100644 --- a/web/lib/test/flow-onboarding.test.ts +++ b/web/lib/test/flow-onboarding.test.ts @@ -113,7 +113,7 @@ describe('software factory onboarding', () => { expect(readFactoryDraft(JSON.stringify(draft))?.agents).toEqual(['windsurf', 'gemini']); }); - it.each(['cursor', 'opencode'] as const)('uses %s throughout a workflow without falling back to Claude', async agent => { + it.each(['grok', 'opencode'] as const)('uses %s throughout a workflow without falling back to Claude', async agent => { const draft = { ...completed, agents: [agent] }; expect(primaryAgent(draft)).toBe(agent); expect(factorySource(draft)).toContain(`const builder = "${agent}"`); @@ -125,14 +125,14 @@ describe('software factory onboarding', () => { expect(calls.some(call => call.endsWith(':claude'))).toBe(false); }); - it('assigns Cursor and OpenCode distinct prototype and review roles', async () => { - const draft: FactoryDraft = { ...completed, agents: ['cursor', 'opencode'], workflow: 'prototype' }; + it('assigns Grok and OpenCode distinct prototype and review roles', async () => { + const draft: FactoryDraft = { ...completed, agents: ['grok', 'opencode'], workflow: 'prototype' }; const { calls } = await runFactory([true], true, matchingIssue, draft); - expect(calls).toContain('prototype-1:cursor'); + expect(calls).toContain('prototype-1:grok'); expect(calls).toContain('prototype-2:opencode'); - expect(calls).toContain('prototype-3:cursor'); + expect(calls).toContain('prototype-3:grok'); expect(calls).toContain('comparator:opencode'); - expect(calls).toContain('implementer:cursor'); + expect(calls).toContain('implementer:grok'); }); it('restores incomplete drafts to the first unanswered question', () => { From 911a722957878bea201ca3bf3c0f81b24255de28 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 19 Sep 2026 17:28:27 -0700 Subject: [PATCH 2/2] feat(flows): offer Cursor instead of OpenCode --- web/app/flows/onboarding/WorkflowPicker.tsx | 4 ++-- web/lib/flow-agents.ts | 4 ++-- web/lib/test/flow-agent-settings.test.ts | 18 +++++++++--------- web/lib/test/flow-analytics.test.ts | 4 ++-- web/lib/test/flow-onboarding.test.ts | 10 +++++----- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/web/app/flows/onboarding/WorkflowPicker.tsx b/web/app/flows/onboarding/WorkflowPicker.tsx index 463545f..11a8041 100644 --- a/web/app/flows/onboarding/WorkflowPicker.tsx +++ b/web/app/flows/onboarding/WorkflowPicker.tsx @@ -7,7 +7,7 @@ import { SiGithub, SiGitlab } from 'react-icons/si'; import Claude from '@lobehub/icons/es/Claude'; import Codex from '@lobehub/icons/es/Codex'; import Grok from '@lobehub/icons/es/Grok'; -import OpenCode from '@lobehub/icons/es/OpenCode'; +import Cursor from '@lobehub/icons/es/Cursor'; import { agentLabel, canContinue, isCodingAgent, type CodingAgent, type FactoryDraft } from '../../../lib/flow-onboarding'; import { repositoryHost, sourceLabel, sourceSummary } from '../../../lib/flow-sources'; import { SourceIcon } from './SourcePicker'; @@ -15,7 +15,7 @@ import type { FlowTrack } from '../../../lib/flow-analytics'; import s from './onboarding.module.css'; function ProcessAgent({ id }: { id: CodingAgent }) { - const Icon = { claude: Claude.Color, codex: Codex.Color, opencode: OpenCode, grok: Grok }[id]; + const Icon = { claude: Claude.Color, codex: Codex.Color, cursor: Cursor, grok: Grok }[id]; return ; } diff --git a/web/lib/flow-agents.ts b/web/lib/flow-agents.ts index cce56da..e38580b 100644 --- a/web/lib/flow-agents.ts +++ b/web/lib/flow-agents.ts @@ -2,8 +2,8 @@ export const CODING_AGENTS = [ { id: 'claude', label: 'Claude Code', available: true }, { id: 'codex', label: 'Codex', available: true }, { id: 'gemini', label: 'Gemini CLI', available: false }, - { id: 'opencode', label: 'OpenCode', available: true }, - { id: 'cursor', label: 'Cursor', available: false }, + { id: 'opencode', label: 'OpenCode', available: false }, + { id: 'cursor', label: 'Cursor', available: true }, { id: 'copilot', label: 'GitHub Copilot', available: false }, { id: 'windsurf', label: 'Windsurf', available: false }, { id: 'aider', label: 'Aider', available: false }, diff --git a/web/lib/test/flow-agent-settings.test.ts b/web/lib/test/flow-agent-settings.test.ts index 624d3f2..faf9130 100644 --- a/web/lib/test/flow-agent-settings.test.ts +++ b/web/lib/test/flow-agent-settings.test.ts @@ -4,7 +4,7 @@ import { DEFAULT_FACTORY, factorySource, readFactoryDraft, cloudConnectionsHref, import { resolveAgentSettings } from '../flow-agent-settings'; import { localKitFiles } from '../flow-local'; -const draft: FactoryDraft = { ...DEFAULT_FACTORY, sources: ['github'], agents: ['claude', 'codex', 'grok', 'opencode'], workflow: 'prototype', step: 3 }; +const draft: FactoryDraft = { ...DEFAULT_FACTORY, sources: ['github'], agents: ['claude', 'codex', 'grok', 'cursor'], workflow: 'prototype', step: 3 }; async function execute(value: FactoryDraft) { const source = factorySource(value).replace('import { flow } from "@relayflows/surface";', ''); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }); @@ -22,25 +22,25 @@ async function execute(value: FactoryDraft) { describe('per-step agent settings', () => { it('inherits CLI models and adapts assignments to the selected agents', () => { - expect(resolveAgentSettings('traditional', 'planner', ['grok', 'opencode'])).toMatchObject({ agent: 'grok', model: '' }); - expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'opencode'])).toMatchObject({ agent: 'opencode', model: '' }); + expect(resolveAgentSettings('traditional', 'planner', ['grok', 'cursor'])).toMatchObject({ agent: 'grok', model: '' }); + expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'cursor'])).toMatchObject({ agent: 'cursor', model: '' }); expect(resolveAgentSettings('prototype', 'prototype-2', ['codex']).agent).toBe('codex'); expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); }); - it('keeps old Cursor settings readable while falling back to a supported agent', () => { - const saved = { 'simple:implementer': { agent: 'cursor' as const, model: 'cursor-model', prompt: 'Custom work' } }; + it('keeps old OpenCode settings readable while falling back to a supported agent', () => { + const saved = { 'simple:implementer': { agent: 'opencode' as const, model: 'opencode-model', prompt: 'Custom work' } }; expect(readFactoryDraft(JSON.stringify({ ...draft, agentSettings: saved })))?.toMatchObject({ agentSettings: saved }); - expect(resolveAgentSettings('simple', 'implementer', ['cursor'], saved)).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); + expect(resolveAgentSettings('simple', 'implementer', ['opencode'], saved)).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); }); it('runs distinct prototype overrides while preserving ticket and worktree context', async () => { const prompt = 'Use "quotes", `ticks`, ${literal}, and a newline.\nWrite prototype-notes.md.'; const calls = await execute({ ...draft, task: 'Keep changes focused.', agentSettings: { 'prototype:prototype-1': { agent: 'grok', model: 'model-one', prompt }, - 'prototype:prototype-2': { agent: 'opencode', model: 'model-two' }, + 'prototype:prototype-2': { agent: 'cursor', model: 'model-two' }, 'prototype:comparator': { agent: 'codex', prompt: 'Write comparison.md.' }, - 'prototype:implementer': { agent: 'opencode', model: 'build-model' }, + 'prototype:implementer': { agent: 'cursor', model: 'build-model' }, } }); expect(calls['prototype-1']).toMatchObject({ cli: 'grok', model: 'model-one', cwd: '/tmp/prototypes/1' }); expect(calls['prototype-1'].task).toContain(prompt); @@ -49,7 +49,7 @@ describe('per-step agent settings', () => { expect(calls['prototype-2'].model).toBe('model-two'); expect(calls['prototype-3'].model).toBeUndefined(); expect(calls.comparator.task).toContain('/tmp/prototypes/1, /tmp/prototypes/2, /tmp/prototypes/3'); - expect(calls.implementer).toMatchObject({ cli: 'opencode', model: 'build-model' }); + expect(calls.implementer).toMatchObject({ cli: 'cursor', model: 'build-model' }); }); it('applies the shared reviewer settings to both traditional rounds', async () => { diff --git a/web/lib/test/flow-analytics.test.ts b/web/lib/test/flow-analytics.test.ts index 4d8636e..138a3a8 100644 --- a/web/lib/test/flow-analytics.test.ts +++ b/web/lib/test/flow-analytics.test.ts @@ -12,8 +12,8 @@ function fixture() { } describe('onboarding funnel accounting', () => { - it('counts Grok and OpenCode as supported agents', () => { - expect(flowMetrics({ ...DEFAULT_FACTORY, agents: ['grok', 'opencode', 'cursor'] })).toMatchObject({ + it('counts Grok and Cursor as supported agents', () => { + expect(flowMetrics({ ...DEFAULT_FACTORY, agents: ['grok', 'cursor', 'opencode'] })).toMatchObject({ supported_agent_count: 2, requested_agent_count: 1, }); }); diff --git a/web/lib/test/flow-onboarding.test.ts b/web/lib/test/flow-onboarding.test.ts index 5bd0810..6b7bab4 100644 --- a/web/lib/test/flow-onboarding.test.ts +++ b/web/lib/test/flow-onboarding.test.ts @@ -113,7 +113,7 @@ describe('software factory onboarding', () => { expect(readFactoryDraft(JSON.stringify(draft))?.agents).toEqual(['windsurf', 'gemini']); }); - it.each(['grok', 'opencode'] as const)('uses %s throughout a workflow without falling back to Claude', async agent => { + it.each(['grok', 'cursor'] as const)('uses %s throughout a workflow without falling back to Claude', async agent => { const draft = { ...completed, agents: [agent] }; expect(primaryAgent(draft)).toBe(agent); expect(factorySource(draft)).toContain(`const builder = "${agent}"`); @@ -125,13 +125,13 @@ describe('software factory onboarding', () => { expect(calls.some(call => call.endsWith(':claude'))).toBe(false); }); - it('assigns Grok and OpenCode distinct prototype and review roles', async () => { - const draft: FactoryDraft = { ...completed, agents: ['grok', 'opencode'], workflow: 'prototype' }; + it('assigns Grok and Cursor distinct prototype and review roles', async () => { + const draft: FactoryDraft = { ...completed, agents: ['grok', 'cursor'], workflow: 'prototype' }; const { calls } = await runFactory([true], true, matchingIssue, draft); expect(calls).toContain('prototype-1:grok'); - expect(calls).toContain('prototype-2:opencode'); + expect(calls).toContain('prototype-2:cursor'); expect(calls).toContain('prototype-3:grok'); - expect(calls).toContain('comparator:opencode'); + expect(calls).toContain('comparator:cursor'); expect(calls).toContain('implementer:grok'); });