hosts/cursor/handlers.ps1
|
# Cursor host package — handler implementations (F-050) # # Per hosts/_contract.md, exposes the 5 contract functions: # - New-CursorLaunchInvocation # - ConvertTo-CursorFlag # - Test-CursorRuntimeInstalled # - Get-CursorSignals # - Install-CursorCrewRuntime # # Cursor's standalone Agent CLI is `cursor-agent` (RESOLVED F-050 clarify 2026-05-28 # via `cursor-agent --help` / `--version` on the implementation machine; v2026.05.28). # # Launch shape (verified from `cursor-agent --help`): # `cursor-agent "<prompt>" --workspace <path>` (interactive Agent mode, matches the # claude/codex/antigravity interactive-launch convention used by `specrew start`). # Relevant flags: # prompt (positional) Initial prompt for the agent # --workspace <path> Workspace directory (defaults to cwd) # -f, --force Force-allow commands unless explicitly denied (alias --yolo) # --trust Trust workspace without prompting (ONLY works with --print/headless) # -p, --print Non-interactive scripting mode (NOT used by specrew start, which is interactive) # --mode plan Read-only/planning mode # Non-interactive support is confirmed (FR-011 → Status=supported), but `specrew start` # launches the INTERACTIVE agent so the developer can drive the lifecycle, hence no --print. Set-StrictMode -Version Latest function New-CursorLaunchInvocation { <# .SYNOPSIS Build the Cursor CLI launch invocation per F-050. .OUTPUTS pscustomobject @{ Binary; Args[]; Notices[]; HostKind = 'cursor' } #> param( [Parameter(Mandatory = $true)][string]$ProjectPath, [Parameter(Mandatory = $true)][string]$Prompt, [Parameter(Mandatory = $true)][string]$Agent, # ignored; Cursor has no --agent flag [bool]$AllowAll = $false, [bool]$UseAutopilot = $false, [bool]$UseRemote = $false ) $hostCmd = Get-Command 'cursor-agent' -ErrorAction SilentlyContinue $resolvedBinary = if ($null -ne $hostCmd) { $hostCmd.Source } else { 'cursor-agent' } $argList = New-Object System.Collections.Generic.List[string] $notices = New-Object System.Collections.Generic.List[string] # Interactive launch shape: `cursor-agent "<prompt>" --workspace <path>` $argList.Add($Prompt) | Out-Null $argList.Add('--workspace') | Out-Null $argList.Add($ProjectPath) | Out-Null if ($AllowAll) { $t = ConvertTo-CursorFlag -SpecrewFlag '--allow-all' foreach ($a in $t.Args) { $argList.Add($a) | Out-Null } if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null } } if ($UseAutopilot) { $t = ConvertTo-CursorFlag -SpecrewFlag '--autopilot' foreach ($a in $t.Args) { $argList.Add($a) | Out-Null } if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null } } if ($UseRemote) { $t = ConvertTo-CursorFlag -SpecrewFlag '--remote' if (-not [string]::IsNullOrWhiteSpace($t.Notice)) { $notices.Add($t.Notice) | Out-Null } } return [pscustomobject]@{ Binary = $resolvedBinary Args = $argList.ToArray() Notices = $notices.ToArray() HostKind = 'cursor' } } function ConvertTo-CursorFlag { <# .SYNOPSIS Translate a Specrew-side flag to Cursor CLI flag(s). .OUTPUTS pscustomobject @{ Args[]; Notice; SuppressWarning } #> param( [Parameter(Mandatory = $true)] [ValidateSet('--remote', '--allow-all', '--autopilot')] [string]$SpecrewFlag ) switch ($SpecrewFlag) { '--remote' { return [pscustomobject]@{ Args = @() Notice = 'Cursor CLI does not expose a remote-control flag today; continuing launch without remote-control wiring.' SuppressWarning = $false } } '--allow-all' { return [pscustomobject]@{ Args = @('--force') Notice = "Translated --allow-all to Cursor's --force (run-everything: force-allow commands unless explicitly denied; verified from cursor-agent --help)." SuppressWarning = $true } } '--autopilot' { return [pscustomobject]@{ Args = @() Notice = "Cursor's run-everything equivalent is --force, already mapped from --allow-all. --autopilot is a no-op when --allow-all is also set." SuppressWarning = $true } } } } function Test-CursorRuntimeInstalled { <# .SYNOPSIS Cursor's Crew runtime convention is .cursor/rules/ (.mdc Project Rules). Detects whether the Crew runtime has been deployed (mirrors codex/antigravity contract: this checks the per-host AgentDir, NOT the binary on PATH — Test-SpecrewHostAvailable probes PATH). .OUTPUTS bool #> param([Parameter(Mandatory = $true)][string]$ProjectPath) $rulesDir = Join-Path $ProjectPath '.cursor\rules' if (-not (Test-Path -LiteralPath $rulesDir -PathType Container)) { return $false } $ruleFiles = @(Get-ChildItem -Path $rulesDir -Filter '*.mdc' -ErrorAction SilentlyContinue) return ($ruleFiles.Count -gt 0) } function Get-CursorSignals { <# .SYNOPSIS Detect Cursor-set environment variables (names set when running INSIDE Cursor's agent). .DESCRIPTION Confidence: medium — the exact env-var set Cursor exports inside cursor-agent is not fully documented. CURSOR_API_KEY is documented (cursor-agent --help auth); CURSOR_AGENT and CURSOR_TRACE_ID are observed. Adjust as Cursor's runtime surface is confirmed. .OUTPUTS string[] — names of env vars that are set #> $signals = @() foreach ($variableName in @('CURSOR_AGENT', 'CURSOR_TRACE_ID', 'CURSOR_API_KEY')) { $value = [Environment]::GetEnvironmentVariable($variableName) if (-not [string]::IsNullOrWhiteSpace($value)) { $signals += $variableName } } return $signals } function ConvertTo-CursorAgentDescription { param([string]$Charter, [string]$Role) return (Get-SpecrewCharterTagline -Charter $Charter -Role $Role) } function Install-CursorCrewRuntime { <# .SYNOPSIS Deploy Specrew's Crew runtime to .cursor/rules/<role>.mdc from canonical .specrew/team/agents/<role>.md. Proposal 108 Slice 9 contract function (F-050 Cursor implementation). .DESCRIPTION Cursor reads .cursor/rules/*.mdc Project Rules (auto-attached context — Cursor has no slash-command surface). Each canonical role-charter becomes an .mdc file with MDC YAML front-matter (description + alwaysApply) followed by the charter body. Reference: Cursor Project Rules (https://cursor.com/docs/context/rules). Confidence: medium — MDC front-matter shape (description/globs/alwaysApply) verified against Cursor's documented rules format; smoke-test on first real use and adjust. .OUTPUTS pscustomobject @{ Actions[]; CrewRuntimePath; Notices[] } #> param( [Parameter(Mandatory = $true)][string]$ProjectPath, [switch]$DryRun ) $actions = New-Object System.Collections.Generic.List[hashtable] $notices = New-Object System.Collections.Generic.List[string] $cursorRulesRoot = Get-SpecrewHostAgentRoot -HostKind 'cursor' -ProjectPath $ProjectPath if (-not (Test-Path -LiteralPath $cursorRulesRoot -PathType Container) -and -not $DryRun) { New-Item -ItemType Directory -Path $cursorRulesRoot -Force | Out-Null } foreach ($role in (Get-SpecrewCanonicalAgentRoles -ProjectPath $ProjectPath)) { $content = Get-SpecrewCanonicalCharterContent -ProjectPath $ProjectPath -RoleName $role if ([string]::IsNullOrWhiteSpace($content)) { $notices.Add("Skipping role '$role': no canonical charter found.") | Out-Null continue } $description = ConvertTo-CursorAgentDescription -Charter $content -Role $role $frontmatterLines = @( '---', ('description: {0}' -f ($description -replace '"', '\"')), 'alwaysApply: false', ('# Specrew-managed: this Cursor rule is generated from .specrew/team/agents/{0}.md' -f $role), ('# DO NOT EDIT HERE. Edit the canonical file at .specrew/team/agents/{0}.md instead.' -f $role), '---', '' ) $frontmatter = $frontmatterLines -join "`n" $target = Join-Path $cursorRulesRoot ("{0}.mdc" -f $role) if (-not (Test-SpecrewManagedFile -Path $target)) { $notices.Add("Preserving user-edited file '$target' (no Specrew-managed marker; delete the file to re-sync from canonical).") | Out-Null $actions.Add(@{ Action = 'preserved'; Path = $target; Role = $role }) | Out-Null continue } $finalContent = $frontmatter + $content if ($DryRun) { $actions.Add(@{ Action = 'would-write'; Path = $target; Role = $role }) | Out-Null } else { [System.IO.File]::WriteAllText($target, $finalContent, [System.Text.UTF8Encoding]::new($false)) $actions.Add(@{ Action = 'written'; Path = $target; Role = $role }) | Out-Null } } return [pscustomobject]@{ Actions = $actions.ToArray() CrewRuntimePath = $cursorRulesRoot Notices = $notices.ToArray() } } |