scripts/internal/continuous-co-review/reviewer-host-catalog.ps1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Get-ContinuousCoReviewCatalogValue { param( [AllowNull()] $Object, [Parameter(Mandatory)] [string] $Name, [AllowNull()] $DefaultValue = $null ) if ($null -eq $Object) { return $DefaultValue } if (Test-ReviewerContractPropertyExists -Object $Object -Name $Name) { $value = Get-ReviewerContractPropertyValue -Object $Object -Name $Name if ($null -ne $value) { return $value } } return $DefaultValue } function Test-ContinuousCoReviewReviewerHostInstalled { param( [Parameter(Mandatory)] [string] $CommandName, [scriptblock] $CommandResolver ) if ($CommandResolver) { return [bool] (& $CommandResolver -CommandName $CommandName) } return ($null -ne (Get-Command -Name $CommandName -ErrorAction SilentlyContinue)) } function Get-ContinuousCoReviewReviewerHostRows { # The canonical reviewer-host catalog (DATA, the ONE place a host is defined). Each row carries the host's # identity + review-class rank AND its AGENTIC invocation for the worktree reviewer: the executable (`command`), # the headless-run args (`agentic_args`), and how the slim prompt is passed (`prompt_via_stdin`). Adding or # changing a reviewer host is a ROW edit here - never an edit to the invocation core (worktree-reviewer.ps1). # Hosts with an empty agentic_args have no headless agentic CLI yet (the worktree model needs one); they remain # selectable/authorizable but are not agentically invokable until their command is filled in. return @( # Keep OAuth/keychain authentication while loading no user/project/local settings, memories, hooks, # skills, or MCPs. The reviewer receives read-only inspection tools plus Write solely for the external # candidate path; it cannot run repository commands that generate files inside the frozen snapshot. @{ host = 'claude'; command = 'claude'; agentic_args = @('-p', '--no-session-persistence', '--setting-sources', '', '--disable-slash-commands', '--no-chrome', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--tools', 'Read,Glob,Grep,Write', '--permission-mode', 'bypassPermissions'); prompt_via_stdin = $true; model = 'opus-4.8-1m-context'; adapter_id = 'reviewer-host-adapter-claude-prompt'; rank = 85; default_timeout_seconds = 600; production_harness_id = 'claude-code-file-primary'; production_constructor = 'New-ReviewClaudeFilePrimaryHarnessPort'; result_transport = 'file-primary'; candidate_contract_version = '1.0' } # codex runs with --dangerously-bypass-approvals-and-sandbox BY DESIGN: the worktree reviewer already runs in an # EPHEMERAL, isolated, read-only-source git-tree worktree (precisely the "externally sandboxed environment" that # flag is documented for). codex's INNER Windows restricted-token sandbox is therefore redundant AND fragile # here — it needs the unique per-run temp worktree registered as a trusted project (codex HANGS headlessly # waiting for that trust) plus its sandbox-setup helper resolvable next to the launcher. Bypassing removes BOTH # failure modes with zero per-run / per-machine config. (F-197 reviewer robustness; drift D-197-I009-009 / T102. # NOTE: reviewing UNTRUSTED third-party code should use the per-run trust-injection mode instead — see T102.) @{ host = 'codex'; command = 'codex'; agentic_args = @('exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'); prompt_via_stdin = $false; model = 'chatgpt'; adapter_id = 'reviewer-host-adapter-codex-exec'; rank = 85; default_timeout_seconds = 600; production_harness_id = 'codex-cli-file-primary'; production_constructor = 'New-ReviewCodexFilePrimaryHarnessPort'; result_transport = 'file-primary'; candidate_contract_version = '1.0' } # copilot headless vector probe-validated live 2026-07-10 (F-198 iteration 001; scratch # probe answered in 6s and exited): `-p <prompt>` is non-interactive exit-after-completion, # --allow-all-tools is REQUIRED for non-interactive mode, --allow-all-paths lets the # reviewer browse its disposable worktree (the ephemeral read-only worktree IS the sandbox, # same doctrine as the codex bypass above), --no-ask-user keeps it autonomous (no ask_user # stalls headless), --no-custom-instructions keeps stray instruction files out of the # composed-prompt contract. The invocation core appends the prompt last, directly after -p. @{ host = 'copilot'; command = 'copilot'; agentic_args = @('--allow-all-tools', '--allow-all-paths', '--no-ask-user', '--no-custom-instructions', '--no-color', '--log-level', 'none', '-p'); prompt_via_stdin = $false; model = 'gpt-5.5-or-claude-4.8'; adapter_id = 'reviewer-host-adapter-copilot-prompt'; rank = 80; default_timeout_seconds = 300; production_harness_id = 'copilot-cli-file-primary'; production_constructor = 'New-ReviewCopilotFilePrimaryHarnessPort'; result_transport = 'file-primary'; candidate_contract_version = '1.0' } # Cursor's installed help confirms --print is non-interactive, --trust suppresses the headless workspace # trust prompt, and --force prevents tool-approval stalls. The common contract appends the prompt last. @{ host = 'cursor-agent'; command = 'cursor-agent'; agentic_args = @('--print', '--trust', '--force'); prompt_via_stdin = $false; model = 'configured-by-user'; adapter_id = 'reviewer-host-adapter-cursor-agent-prompt'; rank = 70; production_harness_id = 'cursor-agent-file-primary'; production_constructor = 'New-ReviewCursorAgentFilePrimaryHarnessPort'; result_transport = 'file-primary'; candidate_contract_version = '1.0' } # antigravity ships as `agy` (verified live on the maintainer machine 2026-07-08). The WORKING # headless vector is probe-validated and ORDER-SENSITIVE: flags BEFORE --print, prompt # POSITIONAL directly after it (the invocation core appends the prompt last): # agy --dangerously-skip-permissions --print-timeout 9m --print "<prompt>" # Any flag AFTER --print is swallowed into the prompt (probe: the model answered an essay about # the flag text; --print-timeout placed after --print hangs). skip-permissions for the same # reason as the codex bypass above (the ephemeral read-only worktree IS the sandbox; headless # permission prompts would hang). --print-timeout 9m lifts agy's 5m internal default above our # review budget so OUR watchdog owns the kill (agy's default truncated a real 310s review to a # partial-harvest salvage). agy's native `models` subcommand is the first real consumer for the # model_probe seam (DEFER-197-I010-002). @{ host = 'antigravity'; command = 'agy'; agentic_args = @('--dangerously-skip-permissions', '--print-timeout', '15m', '--print'); prompt_via_stdin = $false; model = 'configured-by-user'; adapter_id = 'reviewer-host-adapter-antigravity-prompt'; rank = 65; default_timeout_seconds = 900; production_harness_id = 'antigravity-file-primary'; production_constructor = 'New-ReviewAntigravityFilePrimaryHarnessPort'; result_transport = 'file-primary'; candidate_contract_version = '1.0' } ) } function Get-ContinuousCoReviewProductionHarnessDefinition { [CmdletBinding()] param([Parameter(Mandatory)][string]$HostName) $needle = $HostName.ToLowerInvariant() $row = @(Get-ContinuousCoReviewReviewerHostRows) | Where-Object { $_.host -ceq $needle } | Select-Object -First 1 if ($null -eq $row) { return $null } foreach ($required in @('production_harness_id', 'production_constructor', 'result_transport', 'candidate_contract_version')) { if (-not $row.Contains($required)) { return $null } } if ([string]$row.result_transport -cne 'file-primary' -or [string]$row.candidate_contract_version -cne '1.0') { return $null } $defaultTimeout = Get-ContinuousCoReviewHostDefaultTimeoutSeconds -HostName $needle if ($null -eq $defaultTimeout) { $defaultTimeout = 600 } return [pscustomobject][ordered]@{ host = [string]$row.host harness_id = [string]$row.production_harness_id constructor = [string]$row.production_constructor command = [string]$row.command pre_arguments = @($row.agentic_args) prompt_transport = $(if ([bool]$row.prompt_via_stdin) { 'stdin' } else { 'argument' }) result_transport = [string]$row.result_transport candidate_contract_version = [string]$row.candidate_contract_version default_timeout_seconds = [int]$defaultTimeout } } function Get-ContinuousCoReviewHostDefaultTimeoutSeconds { # F-198 FR-022: the per-host review budget from the catalog row (data, the one harness # seam). Values are field-measured (F-198 iteration 001: copilot 61-82s observed -> 300; # codex 240-410s -> 600; claude 600 and antigravity 900 per the maintainer clarify). # Returns $null when the row is absent or carries no value - the tolerant-reader contract: # callers fall through to the 600-second floor, never throw. param([Parameter(Mandatory)][string]$HostName) $needle = ([string]$HostName).ToLowerInvariant() $row = @(Get-ContinuousCoReviewReviewerHostRows) | Where-Object { $_.host -eq $needle } | Select-Object -First 1 if ($null -eq $row) { return $null } if (-not $row.Contains('default_timeout_seconds')) { return $null } $value = 0 if ([int]::TryParse([string]$row['default_timeout_seconds'], [ref]$value) -and $value -gt 0) { return $value } return $null } function Get-ContinuousCoReviewNavigatorTimeoutSeconds { # Shared by the navigator and the public campaign door. Explicit flags are handled by callers; # this function owns the remaining config -> host catalog -> 600-second floor chain so the two # execution paths cannot silently diverge. The config read fails closed at the authority timing # ceiling that also validates invocation parameters. param([Parameter(Mandatory)][string]$RepoRoot, [int]$Default = 600, [AllowNull()][string]$HostName) $maxTimeoutSeconds = [int](Get-ReviewAuthorityTimingLimits).max_invocation_timeout_seconds $configPath = Join-Path $RepoRoot '.specrew/config.yml' if (Test-Path -LiteralPath $configPath -PathType Leaf) { $configLines = @() try { $configLines = @(Get-Content -LiteralPath $configPath -Encoding UTF8) } catch { $null = $_ } foreach ($line in $configLines) { if ($line -match '^\s*co_review_timeout_seconds:\s*[''"]?(?<value>[^''"#]+?)[''"]?\s*(?:#.*)?$') { $parsed = 0 if ([int]::TryParse(($Matches['value'].Trim()), [ref]$parsed) -and $parsed -gt 0) { if ($parsed -gt $maxTimeoutSeconds) { throw ('co-review-timeout-exceeds-maximum:{0}:{1}' -f $parsed, $maxTimeoutSeconds) } return $parsed } } } } if (-not [string]::IsNullOrWhiteSpace($HostName)) { $catalogValue = Get-ContinuousCoReviewHostDefaultTimeoutSeconds -HostName $HostName if ($null -ne $catalogValue -and [int]$catalogValue -gt 0) { return [int]$catalogValue } } return $Default } function Get-ContinuousCoReviewHostAgenticCommand { # The agentic invocation for a reviewer host, looked up from the catalog DATA above (host-NEUTRAL: the invocation # core calls this instead of switching on host names). Returns @{ file; pre_args; prompt_via_stdin } or $null # (host not in the catalog, or no agentic command defined). Adding a host's command is a catalog-row edit. param([Parameter(Mandatory)][string]$HostName) $needle = ([string]$HostName).ToLowerInvariant() $row = @(Get-ContinuousCoReviewReviewerHostRows) | Where-Object { $_.host -eq $needle } | Select-Object -First 1 if ($null -eq $row -or @($row.agentic_args).Count -eq 0) { return $null } return [pscustomobject]@{ file = [string]$row.command; pre_args = @($row.agentic_args); prompt_via_stdin = [bool]$row.prompt_via_stdin } } function New-ContinuousCoReviewDefaultReviewerHostConfig { param( [scriptblock] $CommandResolver ) $hostRows = Get-ContinuousCoReviewReviewerHostRows return [pscustomobject][ordered]@{ schema_version = '1.0' hosts = @( foreach ($row in $hostRows) { [pscustomobject][ordered]@{ host = $row.host model = $row.model adapter_id = $row.adapter_id allowed = $false installed = (Test-ContinuousCoReviewReviewerHostInstalled -CommandName $row.command -CommandResolver $CommandResolver) review_class_rank = [int] $row.rank model_source = 'human-entered' cost_class = 'non-default' authorization_ref = $null fallback_allowed = $false } } ) } } function ConvertTo-ContinuousCoReviewReviewerHostCatalogEntry { param( [Parameter(Mandatory)] $Entry ) $adapterId = [string] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'adapter_id') if ($adapterId -notmatch '^reviewer-host-adapter-[a-z0-9-]+$') { throw "Reviewer host catalog entry uses an invalid adapter id '$adapterId'." } return [pscustomobject][ordered]@{ host = [string] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'host') model = [string] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'model') adapter_id = $adapterId allowed = [bool] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'allowed' -DefaultValue $false) installed = [bool] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'installed' -DefaultValue $false) review_class_rank = [int] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'review_class_rank' -DefaultValue 0) model_source = [string] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'model_source' -DefaultValue 'human-entered') cost_class = [string] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'cost_class' -DefaultValue 'non-default') authorization_ref = Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'authorization_ref' fallback_allowed = [bool] (Get-ContinuousCoReviewCatalogValue -Object $Entry -Name 'fallback_allowed' -DefaultValue $false) } } function Get-ContinuousCoReviewReviewerHostCatalog { param( [AllowNull()] $Configuration, [scriptblock] $CommandResolver ) $resolvedConfiguration = if ($null -eq $Configuration) { New-ContinuousCoReviewDefaultReviewerHostConfig -CommandResolver $CommandResolver } else { $Configuration } $hosts = @( foreach ($entry in @($resolvedConfiguration.hosts)) { ConvertTo-ContinuousCoReviewReviewerHostCatalogEntry -Entry $entry } ) return [pscustomobject][ordered]@{ schema_version = '1.0' hosts = @($hosts) } } |