diff --git a/src/__tests__/runtime-guidance-plan-ralph.test.ts b/src/__tests__/runtime-guidance-plan-ralph.test.ts index 5c254c3ea9..d4b4567da9 100644 --- a/src/__tests__/runtime-guidance-plan-ralph.test.ts +++ b/src/__tests__/runtime-guidance-plan-ralph.test.ts @@ -1,13 +1,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { CliAgentType } from '../team/model-contract.js'; const availability = vi.hoisted(() => ({ claude: true, codex: false, gemini: false, + cursor: false, })); vi.mock('../team/model-contract.js', () => ({ - isCliAvailable: (agentType: 'claude' | 'codex' | 'gemini') => availability[agentType], + isCliAvailable: (agentType: CliAgentType) => availability[agentType], })); import { @@ -20,6 +22,7 @@ describe('runtime-guidance: ralplan/plan/ralph Codex availability', () => { availability.claude = true; availability.codex = false; availability.gemini = false; + availability.cursor = false; }); describe('renderSkillRuntimeGuidance for plan-family skills', () => { @@ -75,7 +78,7 @@ describe('runtime-guidance: ralplan/plan/ralph Codex availability', () => { describe('detectSkillRuntimeAvailability safety', () => { it('returns false for a provider whose detector throws instead of crashing', () => { - const throwingDetector = (agentType: 'claude' | 'codex' | 'gemini') => { + const throwingDetector = (agentType: CliAgentType) => { if (agentType === 'codex') { throw new Error( 'External LLM provider "codex" is blocked by security policy (disableExternalLLM).', diff --git a/src/cli/team.ts b/src/cli/team.ts index def0629199..f3054701a0 100644 --- a/src/cli/team.ts +++ b/src/cli/team.ts @@ -14,7 +14,7 @@ import { isProcessAlive } from '../platform/index.js'; import { getGlobalOmcStatePath } from '../utils/paths.js'; const JOB_ID_PATTERN = /^omc-[a-z0-9]{1,16}$/; -const VALID_CLI_AGENT_TYPES = new Set(['claude', 'codex', 'gemini']); +const VALID_CLI_AGENT_TYPES = new Set(['claude', 'codex', 'gemini', 'cursor']); const SUBCOMMANDS = new Set(['start', 'status', 'wait', 'cleanup', 'resume', 'shutdown', 'api', 'help', '--help', '-h']); const SUPPORTED_API_OPERATIONS = new Set([ @@ -685,7 +685,7 @@ export async function teamCleanupCommand( export const TEAM_USAGE = ` Usage: - omc team start --agent [,...] --task "" [--count N] [--name TEAM] [--cwd DIR] [--new-window] [--json] + omc team start --agent [,...] --task "" [--count N] [--name TEAM] [--cwd DIR] [--new-window] [--json] omc team status [--json] [--cwd DIR] omc team wait [--timeout-ms MS] [--json] omc team cleanup [--grace-ms MS] [--json] diff --git a/src/team/capabilities.ts b/src/team/capabilities.ts index 892b53afc7..d0bfbd541c 100644 --- a/src/team/capabilities.ts +++ b/src/team/capabilities.ts @@ -18,6 +18,7 @@ const DEFAULT_CAPABILITIES: Record = { 'tmux-claude': ['code-edit', 'testing', 'general'], 'tmux-codex': ['code-review', 'security-review', 'architecture', 'refactoring'], 'tmux-gemini': ['ui-design', 'documentation', 'research', 'code-edit'], + 'tmux-cursor': ['code-edit', 'refactoring', 'general'], }; /** diff --git a/src/team/cli-detection.ts b/src/team/cli-detection.ts index a7f6f4122f..e4a21107e6 100644 --- a/src/team/cli-detection.ts +++ b/src/team/cli-detection.ts @@ -35,5 +35,6 @@ export function detectAllClis(): Record { claude: detectCli('claude'), codex: detectCli('codex'), gemini: detectCli('gemini'), + cursor: detectCli('cursor-agent'), }; } diff --git a/src/team/cli-worker-contract.ts b/src/team/cli-worker-contract.ts index d76eade8dc..dac44a8bfd 100644 --- a/src/team/cli-worker-contract.ts +++ b/src/team/cli-worker-contract.ts @@ -57,7 +57,14 @@ export function shouldInjectContract( provider: CliAgentType | null | undefined, ): boolean { if (!role || !provider) return false; - if (provider === 'claude') return false; + // Claude workers speak through the team messaging API directly. + // Cursor workers run as interactive REPLs — they cannot perform the + // write-verdict-and-exit dance the contract requires, so reviewer + // roles must not be assigned to cursor in the first place. The + // role-router and worker-bootstrap guidance both flag this; here we + // simply skip contract injection if a cursor worker somehow lands on + // a CONTRACT_ROLES role rather than emit instructions it cannot follow. + if (provider === 'claude' || provider === 'cursor') return false; return CONTRACT_ROLES.has(role); } diff --git a/src/team/model-contract.ts b/src/team/model-contract.ts index f8324dd837..7a88c3d697 100644 --- a/src/team/model-contract.ts +++ b/src/team/model-contract.ts @@ -5,7 +5,7 @@ import { normalizeToCcAlias } from '../features/delegation-enforcer.js'; import { isBedrock, isVertexAI, isProviderSpecificModelId } from '../config/models.js'; import { isExternalLLMDisabled } from '../lib/security-config.js'; -export type CliAgentType = 'claude' | 'codex' | 'gemini'; +export type CliAgentType = 'claude' | 'codex' | 'gemini' | 'cursor'; export interface CliAgentContract { agentType: CliAgentType; @@ -224,6 +224,24 @@ const CONTRACTS: Record = { return rawOutput.trim(); }, }, + cursor: { + agentType: 'cursor', + binary: 'cursor-agent', + installInstructions: 'Install Cursor Agent CLI: see https://docs.cursor.com/cli', + // cursor-agent runs as an interactive REPL — no exit-on-complete prompt mode. + // Keep supportsPromptMode false so the verdict-file contract path + // (CONTRACT_ROLES + shouldInjectContract) skips this provider; cursor + // workers participate as executors only. + supportsPromptMode: false, + buildLaunchArgs(_model?: string, extraFlags: string[] = []): string[] { + // Minimal flags — cursor-agent owns its own session/auth state. + // The model is selected interactively inside cursor-agent itself. + return [...extraFlags]; + }, + parseOutput(rawOutput: string): string { + return rawOutput.trim(); + }, + }, }; export function getContract(agentType: CliAgentType): CliAgentContract { diff --git a/src/team/types.ts b/src/team/types.ts index a5eb8719f5..319d418f81 100644 --- a/src/team/types.ts +++ b/src/team/types.ts @@ -138,7 +138,7 @@ export interface TaskFailureSidecar { } /** Worker backend type */ -export type WorkerBackend = 'claude-native' | 'mcp-codex' | 'mcp-gemini' | 'tmux-claude' | 'tmux-codex' | 'tmux-gemini'; +export type WorkerBackend = 'claude-native' | 'mcp-codex' | 'mcp-gemini' | 'tmux-claude' | 'tmux-codex' | 'tmux-gemini' | 'tmux-cursor'; /** Worker capability tag */ export type WorkerCapability = @@ -253,7 +253,7 @@ export interface WorkerInfo { name: string; index: number; role: string; - worker_cli?: 'codex' | 'claude' | 'gemini'; + worker_cli?: 'codex' | 'claude' | 'gemini' | 'cursor'; assigned_tasks: string[]; pid?: number; pane_id?: string; diff --git a/src/team/worker-bootstrap.ts b/src/team/worker-bootstrap.ts index a49b48e5ba..7695c7541d 100644 --- a/src/team/worker-bootstrap.ts +++ b/src/team/worker-bootstrap.ts @@ -73,6 +73,13 @@ function agentTypeGuidance(agentType: CliAgentType): string { '- Keep commit-sized changes scoped to assigned files only; no broad refactors.', `- CRITICAL: You MUST run \`${claimTaskCommand}\` before starting work and \`${transitionTaskStatusCommand}\` when done. Do not exit without transitioning the task status.`, ].join('\n'); + case 'cursor': + return [ + '### Agent-Type Guidance (cursor)', + '- You are an interactive REPL (cursor-agent), not a one-shot CLI. Stay in the session; the leader will continue to send prompts via mailbox.', + `- You MUST run \`${claimTaskCommand}\` before starting work and \`${transitionTaskStatusCommand}\` when done. Then keep waiting for the next mailbox message; do NOT type \`/exit\` unless the leader sends an explicit shutdown.`, + '- Reviewer/critic/security-review roles are NOT supported for cursor workers — those require a verdict-file write-and-exit which the REPL does not perform. Take only executor-style tasks.', + ].join('\n'); case 'claude': default: return [