param(
  [string]$ProjectRoot = "",
  [string]$TaskFile = "task.json",
  [string]$ProgressFile = "progress.txt",
  [string]$TraceDir = "traces",
  [string]$CodexCommand = "codex",
  [string]$RunProfileFile = ".codex\\task-run-profile.json",
  [switch]$RunUntilDone,
  [switch]$CaptureJsonEvents
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[Console]::OutputEncoding = $utf8NoBom
$OutputEncoding = $utf8NoBom

if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
  $ProjectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\.."))
}

function Write-Step {
  <#
    输出简洁的 driver 状态，避免把 Codex 原始事件流刷满终端。
  #>
  param([string]$Message)

  $timestamp = Get-Date -Format "HH:mm:ss"
  Write-Host "[$timestamp] $Message"
}

function Get-TextFileContent {
  <#
    读取 UTF-8 文本文件，文件不存在时返回空字符串。
  #>
  param([string]$Path)

  if (-not (Test-Path -LiteralPath $Path)) {
    return ""
  }

  return Get-Content -LiteralPath $Path -Raw -Encoding UTF8
}

function Get-HarnessStateConfigPath {
  <#
    返回 harness 安装状态配置文件路径。
  #>
  param([string]$Root)

  return (Join-Path $Root ".codex-harness\state\config.json")
}

function Get-HarnessPackageRoot {
  <#
    读取 thin project 对应的用户级 packageRoot；不存在时返回空字符串。
  #>
  param([string]$Root)

  $configPath = Get-HarnessStateConfigPath -Root $Root
  if (-not (Test-Path -LiteralPath $configPath)) {
    return ""
  }

  try {
    $configDocument = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
    $packageRoot = [string](Get-ObjectPropertyValue -InputObject $configDocument -Name "packageRoot" -Default "")
    if ([string]::IsNullOrWhiteSpace($packageRoot)) {
      return ""
    }

    return [System.IO.Path]::GetFullPath($packageRoot)
  }
  catch {
    return ""
  }
}

function Resolve-HarnessProjectOrPackagePath {
  <#
    优先解析项目内路径；若 thin project 未安装该文档，则回退到 packageRoot。
  #>
  param(
    [string]$Root,
    [string]$RelativePath
  )

  if ([string]::IsNullOrWhiteSpace($RelativePath)) {
    return $null
  }

  if ([System.IO.Path]::IsPathRooted($RelativePath)) {
    return $RelativePath
  }

  $normalizedRelativePath = $RelativePath.Replace("/", "\")
  $projectPath = Join-Path $Root $normalizedRelativePath
  if (Test-Path -LiteralPath $projectPath) {
    return $projectPath
  }

  $packageRoot = Get-HarnessPackageRoot -Root $Root
  if (-not [string]::IsNullOrWhiteSpace($packageRoot)) {
    $packagePath = Join-Path $packageRoot $normalizedRelativePath
    if (Test-Path -LiteralPath $packagePath) {
      return $packagePath
    }
  }

  return $projectPath
}

function Get-HarnessDisplayReference {
  <#
    返回给 prompt / reviewer 显示的引用路径；项目内不存在时显示 package 绝对路径。
  #>
  param(
    [string]$Root,
    [string]$RelativePath
  )

  $resolvedPath = Resolve-HarnessProjectOrPackagePath -Root $Root -RelativePath $RelativePath
  if ([string]::IsNullOrWhiteSpace($resolvedPath)) {
    return $RelativePath
  }

  $projectRootFull = ([System.IO.Path]::GetFullPath($Root)).TrimEnd('\', '/') + '\'
  $resolvedFull = [System.IO.Path]::GetFullPath($resolvedPath)
  if ($resolvedFull.StartsWith($projectRootFull, [System.StringComparison]::OrdinalIgnoreCase)) {
    return $RelativePath.Replace("/", "\")
  }

  return $resolvedFull
}

function ConvertTo-HarnessDisplayReferences {
  <#
    批量解析给 prompt 展示的 truth source / context source 路径。
  #>
  param(
    [string]$Root,
    [string[]]$RelativePaths
  )

  $references = @()
  foreach ($relativePath in @(ConvertTo-StringArray -Value $RelativePaths)) {
    $references += Get-HarnessDisplayReference -Root $Root -RelativePath $relativePath
  }

  return @(ConvertTo-UniqueStringArray -Items $references)
}

function Show-LogTail {
  <#
    失败时只显示最后几行日志，并保留完整日志文件供排查。
  #>
  param(
    [string]$Path,
    [int]$LineCount = 40
  )

  if (-not (Test-Path -LiteralPath $Path)) {
    return
  }

  Write-Output ""
  Write-Output "最近 $LineCount 行日志:"
  Get-Content -LiteralPath $Path -Tail $LineCount -Encoding UTF8
}

function Get-LastNonEmptyLine {
  <#
    返回日志文件最后一条非空文本，缺失时返回空字符串。
  #>
  param([string]$Path)

  if (-not (Test-Path -LiteralPath $Path)) {
    return ""
  }

  $lines = Get-Content -LiteralPath $Path -Tail 20 -Encoding UTF8
  if ($null -eq $lines) {
    return ""
  }

  foreach ($line in (@($lines)[-1..-([Math]::Min(@($lines).Count, 20))])) {
    if (-not [string]::IsNullOrWhiteSpace($line)) {
      return $line.Trim()
    }
  }

  return ""
}

function Get-CompactLogPreview {
  <#
    压缩日志预览，避免心跳消息过长。
  #>
  param(
    [string]$Path,
    [int]$MaxLength = 120
  )

  $line = Get-LastNonEmptyLine -Path $Path
  if ([string]::IsNullOrWhiteSpace($line)) {
    return ""
  }

  if ($line.Length -le $MaxLength) {
    return $line
  }

  return ($line.Substring(0, $MaxLength - 3) + "...")
}

function Get-ObjectPropertyValue {
  <#
    安全读取对象属性，属性不存在时返回默认值。
  #>
  param(
    [object]$InputObject,
    [string]$Name,
    $Default = $null
  )

  if ($null -eq $InputObject) {
    return $Default
  }

  $property = $InputObject.PSObject.Properties[$Name]
  if ($null -eq $property) {
    return $Default
  }

  return $property.Value
}

function ConvertTo-StringArray {
  <#
    归一化为字符串数组。
  #>
  param([object]$Value)

  if ($null -eq $Value) {
    return @()
  }

  if ($Value -is [string]) {
    if ([string]::IsNullOrWhiteSpace($Value)) {
      return @()
    }

    return @($Value)
  }

  if ($Value -is [System.Collections.IEnumerable]) {
    $result = @()
    foreach ($item in $Value) {
      if ($null -eq $item) {
        continue
      }

      $text = [string]$item
      if (-not [string]::IsNullOrWhiteSpace($text)) {
        $result += $text
      }
    }

    return $result
  }

  return @([string]$Value)
}

function Resolve-CodexStartProcessPath {
  <#
    在 Windows 下把 codex 命令解析成适合 Start-Process 的可执行文件。
    避免直接把 npm 生成的 codex.ps1 传给 Start-Process。
  #>
  param([string]$Command)

  if ([string]::IsNullOrWhiteSpace($Command)) {
    throw "CodexCommand 不能为空。"
  }

  $resolvedCommand = $null
  try {
    $resolvedCommand = Get-Command $Command -ErrorAction Stop | Select-Object -First 1
  }
  catch {
    return $Command
  }

  $resolvedPath = [string]$resolvedCommand.Path
  if ([string]::IsNullOrWhiteSpace($resolvedPath)) {
    return $Command
  }

  if (-not $resolvedPath.EndsWith(".ps1", [System.StringComparison]::OrdinalIgnoreCase)) {
    return $resolvedPath
  }

  $basePath = ([System.IO.Path]::ChangeExtension($resolvedPath, $null)).TrimEnd('.')
  foreach ($candidate in @("$basePath.cmd", "$basePath.exe")) {
    if (Test-Path -LiteralPath $candidate -PathType Leaf) {
      return $candidate
    }
  }

  return $resolvedPath
}

function Normalize-GitRelativePath {
  <#
    统一 Git 相对路径，便于做 owned_paths / runtime allowlist 比较。
  #>
  param([string]$Path)

  $rawValue = if ($null -eq $Path) { "" } else { [string]$Path }
  $value = $rawValue.Trim().Replace('\', '/')
  while ($value.StartsWith('./', [System.StringComparison]::Ordinal)) {
    $value = $value.Substring(2)
  }

  return $value.TrimStart('/')
}

function Convert-OwnedPathsToPrefixes {
  <#
    读取当前任务声明的 owned_paths，并转成标准前缀。
  #>
  param([object]$Task)

  $ownedPaths = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "owned_paths")
  return @(
    $ownedPaths |
      ForEach-Object { Normalize-GitRelativePath -Path $_ } |
      Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
  )
}

function Convert-ForbiddenPathsToPrefixes {
  <#
    读取当前任务声明的 forbidden_paths，并转成标准前缀。
  #>
  param([object]$Task)

  $forbiddenPaths = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "forbidden_paths")
  return @(
    $forbiddenPaths |
      ForEach-Object { Normalize-GitRelativePath -Path $_ } |
      Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
  )
}

function Convert-GitStatusLineToRelativePaths {
  <#
    从 porcelain v1 -z record 提取一个或两个受影响路径。
  #>
  param(
    [string]$Line,
    [string]$NextRecord = $null
  )

  if ([string]::IsNullOrWhiteSpace($Line) -or $Line.Length -lt 4) {
    return @()
  }

  $statusCode = $Line.Substring(0, 2)
  $relativePath = $Line.Substring(3)
  if ([string]::IsNullOrWhiteSpace($relativePath)) {
    return @()
  }

  $paths = @()
  $isRenameOrCopy = ($statusCode.IndexOf('R') -ge 0 -or $statusCode.IndexOf('C') -ge 0)
  if ($isRenameOrCopy) {
    $sourcePath = Normalize-GitRelativePath -Path $NextRecord
    $destinationPath = Normalize-GitRelativePath -Path $relativePath
    if (-not [string]::IsNullOrWhiteSpace($sourcePath)) {
      $paths += $sourcePath
    }
    if (-not [string]::IsNullOrWhiteSpace($destinationPath)) {
      $paths += $destinationPath
    }
    return ConvertTo-UniqueStringArray -Items $paths
  }

  $normalizedPath = Normalize-GitRelativePath -Path $relativePath
  if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
    return @()
  }

  return @($normalizedPath)
}

function Get-GitStatusPorcelainRecords {
  <#
    读取 porcelain v1 -z 输出，避免 core.quotePath 的 C-style escape。
  #>
  param([string]$Root)

  $processInfo = New-Object System.Diagnostics.ProcessStartInfo
  $processInfo.FileName = "git"
  $processInfo.Arguments = "status --porcelain=v1 -z --untracked-files=all"
  $processInfo.WorkingDirectory = $Root
  $processInfo.UseShellExecute = $false
  $processInfo.RedirectStandardOutput = $true
  $processInfo.RedirectStandardError = $true

  $process = [System.Diagnostics.Process]::Start($processInfo)
  $outputStream = New-Object System.IO.MemoryStream
  $exitCode = $null
  try {
    $process.StandardOutput.BaseStream.CopyTo($outputStream)
    $stderr = $process.StandardError.ReadToEnd()
    $process.WaitForExit()
    $exitCode = $process.ExitCode
  }
  finally {
    $process.Dispose()
  }

  if ($exitCode -ne 0) {
    throw "无法读取 Git 状态，无法执行 commit ownership gate。$stderr"
  }

  $text = [System.Text.Encoding]::UTF8.GetString($outputStream.ToArray())
  return @(
    $text.Split([char]0) |
      Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
  )
}

function Get-GitChangedPaths {
  <#
    基于 porcelain 记录提取当前工作区受影响路径列表。
  #>
  param([string]$Root)

  $paths = @()
  $statusLines = @(Get-GitStatusPorcelainRecords -Root $Root)
  for ($i = 0; $i -lt @($statusLines).Count; $i++) {
    $line = $statusLines[$i]
    $nextRecord = $null
    if ($line.Length -ge 2) {
      $statusCode = $line.Substring(0, 2)
      if (($statusCode.IndexOf('R') -ge 0 -or $statusCode.IndexOf('C') -ge 0) -and ($i + 1) -lt @($statusLines).Count) {
        $i++
        $nextRecord = $statusLines[$i]
      }
    }

    $paths += @(Convert-GitStatusLineToRelativePaths -Line $line -NextRecord $nextRecord)
  }

  return @(ConvertTo-UniqueStringArray -Items $paths)
}

function Test-CommitPathOwnership {
  <#
    提交前检查 Git 脏文件是否全部落在 owned_paths 或 runtime allowlist 内。
  #>
  param(
    [string]$Root,
    [object]$Task,
    [string[]]$RuntimeAllowedPaths,
    [string[]]$NonBlockingDirtyPaths
  )

  $allowed = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
  $forbidden = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
  foreach ($path in @(
      (Convert-OwnedPathsToPrefixes -Task $Task) +
      (ConvertTo-StringArray -Value $RuntimeAllowedPaths) +
      (ConvertTo-StringArray -Value $NonBlockingDirtyPaths)
    )) {
    $normalized = Normalize-GitRelativePath -Path $path
    if (-not [string]::IsNullOrWhiteSpace($normalized)) {
      $null = $allowed.Add($normalized)
    }
  }

  foreach ($path in @(Convert-ForbiddenPathsToPrefixes -Task $Task)) {
    $normalized = Normalize-GitRelativePath -Path $path
    if (-not [string]::IsNullOrWhiteSpace($normalized)) {
      $null = $forbidden.Add($normalized)
    }
  }

  $statusLines = @(Get-GitStatusPorcelainRecords -Root $Root)
  $unexpected = New-Object System.Collections.Generic.List[string]
  $forbiddenHits = New-Object System.Collections.Generic.List[string]
  for ($i = 0; $i -lt @($statusLines).Count; $i++) {
    $line = $statusLines[$i]
    $nextRecord = $null
    if ($line.Length -ge 2) {
      $statusCode = $line.Substring(0, 2)
      if (($statusCode.IndexOf('R') -ge 0 -or $statusCode.IndexOf('C') -ge 0) -and ($i + 1) -lt @($statusLines).Count) {
        $i++
        $nextRecord = $statusLines[$i]
      }
    }

    foreach ($relativePath in @(Convert-GitStatusLineToRelativePaths -Line $line -NextRecord $nextRecord)) {
      $forbiddenMatched = $false
      foreach ($forbiddenPath in $forbidden) {
        if (
          $relativePath.Equals($forbiddenPath, [System.StringComparison]::OrdinalIgnoreCase) -or
          $relativePath.StartsWith(($forbiddenPath.TrimEnd('/') + '/'), [System.StringComparison]::OrdinalIgnoreCase)
        ) {
          $forbiddenMatched = $true
          break
        }
      }

      if ($forbiddenMatched) {
        $forbiddenHits.Add($relativePath)
        continue
      }

      $matched = $false
      foreach ($allowedPath in $allowed) {
        if (
          $relativePath.Equals($allowedPath, [System.StringComparison]::OrdinalIgnoreCase) -or
          $relativePath.StartsWith(($allowedPath.TrimEnd('/') + '/'), [System.StringComparison]::OrdinalIgnoreCase)
        ) {
          $matched = $true
          break
        }
      }

      if (-not $matched) {
        $unexpected.Add($relativePath)
      }
    }
  }

  return [PSCustomObject]@{
    Passed = ($unexpected.Count -eq 0 -and $forbiddenHits.Count -eq 0)
    UnexpectedPaths = ConvertTo-UniqueStringArray -Items @($unexpected)
    ForbiddenPaths = ConvertTo-UniqueStringArray -Items @($forbiddenHits)
  }
}

function Test-DependencyChangeApproval {
  <#
    检查依赖清单或 lockfile 变更是否在当前任务允许范围内。
  #>
  param(
    [object]$Task,
    [string[]]$ChangedPaths
  )

  $allowedDependencies = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "allowed_dependencies" -Default @()))
  $dependencyFiles = @(
    "package.json",
    "package-lock.json",
    "pnpm-lock.yaml",
    "yarn.lock",
    "bun.lockb"
  )

  $changedDependencyFiles = New-Object System.Collections.Generic.List[string]
  foreach ($path in @(ConvertTo-StringArray -Value $ChangedPaths)) {
    $normalized = Normalize-GitRelativePath -Path $path
    foreach ($dependencyFile in $dependencyFiles) {
      if ($normalized.Equals($dependencyFile, [System.StringComparison]::OrdinalIgnoreCase) -or $normalized.EndsWith("/$dependencyFile", [System.StringComparison]::OrdinalIgnoreCase)) {
        $changedDependencyFiles.Add($normalized)
        break
      }
    }
  }

  return [PSCustomObject]@{
    Passed = ($changedDependencyFiles.Count -eq 0 -or $allowedDependencies.Count -gt 0)
    ChangedFiles = ConvertTo-UniqueStringArray -Items @($changedDependencyFiles)
    AllowedDependencies = $allowedDependencies
  }
}

function ConvertTo-BulletLines {
  <#
    把数组格式化成多行 bullet，空数组时输出占位。
  #>
  param(
    [string[]]$Items,
    [string]$EmptyText = "- 无"
  )

  if ($null -eq $Items -or $Items.Count -eq 0) {
    return $EmptyText
  }

  return ($Items | ForEach-Object { "- $_" }) -join "`n"
}

function ConvertTo-PromptJson {
  <#
    把任务内的结构化合同序列化进 prompt，避免内层会话只看到字段名称。
  #>
  param(
    [object]$Value,
    [string]$EmptyText = "{}"
  )

  if ($null -eq $Value) {
    return $EmptyText
  }

  return ($Value | ConvertTo-Json -Depth 20)
}

function ConvertTo-UniqueStringArray {
  <#
    去重并保持原始顺序。
  #>
  param([string[]]$Items)

  $result = @()
  foreach ($item in @(ConvertTo-StringArray -Value $Items)) {
    if ($result -notcontains $item) {
      $result += $item
    }
  }

  return $result
}

function Get-TaskDefinitionDefaults {
  <#
    根据任务类型返回默认 gate 和 truth source 约束。
  #>
  param([object]$Task)

  $taskKind = [string](Get-ObjectPropertyValue -InputObject $Task -Name "task_kind" -Default "harness")
  if ([string]::IsNullOrWhiteSpace($taskKind)) {
    $taskKind = "harness"
  }

  $defaultGateProfile = "lightweight"
  $defaultTruthSources = @()

  switch ($taskKind) {
    "harness" {
      $defaultGateProfile = "lightweight"
      $defaultTruthSources = @()
    }
    "smoke" {
      $defaultGateProfile = "lightweight"
      $defaultTruthSources = @()
    }
    "feature_research" {
      $defaultGateProfile = "research_required"
      $defaultTruthSources = @("repo_context")
    }
    "feature_spec" {
      $defaultGateProfile = "research_required"
      $defaultTruthSources = @("repo_context")
    }
    "feature_design" {
      $defaultGateProfile = "spec_required"
      $defaultTruthSources = @("product", "testing")
    }
    "testcase_design" {
      $defaultGateProfile = "spec_required"
      $defaultTruthSources = @("product", "testing")
    }
    "feature_plan" {
      $defaultGateProfile = "spec_required"
      $defaultTruthSources = @("product", "design", "testing", "repo_context")
    }
    "feature_impl" {
      $defaultGateProfile = "spec_required"
      $defaultTruthSources = @("product", "design", "plan", "testing", "repo_context")
    }
    "release" {
      $defaultGateProfile = "release_required"
      $defaultTruthSources = @("product", "design", "plan", "testing", "repo_context")
    }
    "archive" {
      $defaultGateProfile = "lightweight"
      $defaultTruthSources = @("knowledge")
    }
    default {
      $defaultGateProfile = "lightweight"
      $defaultTruthSources = @()
    }
  }

  $gateProfile = [string](Get-ObjectPropertyValue -InputObject $Task -Name "gate_profile" -Default $defaultGateProfile)
  if ([string]::IsNullOrWhiteSpace($gateProfile)) {
    $gateProfile = $defaultGateProfile
  }

  $requiredTruthSources = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "required_truth_sources"))
  if ($requiredTruthSources.Count -eq 0) {
    $requiredTruthSources = @($defaultTruthSources)
  }

  if ($gateProfile -eq "contract_required" -and $requiredTruthSources -notcontains "contract") {
    $requiredTruthSources += "contract"
  }

  $producesArtifacts = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "produces_artifacts")
  $phase = [string](Get-ObjectPropertyValue -InputObject $Task -Name "phase" -Default "")

  return [PSCustomObject]@{
    task_kind = $taskKind
    gate_profile = $gateProfile
    required_truth_sources = ConvertTo-UniqueStringArray -Items $requiredTruthSources
    produces_artifacts = ConvertTo-UniqueStringArray -Items $producesArtifacts
    phase = $phase
  }
}

function Get-DefaultRunProfile {
  <#
    返回默认 task 会话与子代理策略。
  #>
  return [PSCustomObject]@{
    session = [PSCustomObject]@{
      mode = "fresh-process"
      reuse_previous_chat = $false
      carry_forward_summary = $false
      context_sources = @(
        "AGENTS.md",
        "docs/harness/task-session-strategy.md",
        "docs/harness/architecture.md",
        "docs/harness/knowledge-architecture.md",
        "docs/harness/prompt-knowledge-integration.md",
        "docs/harness/rule-governance.md",
        "docs/harness/regression-rules.md",
        "docs/knowledge/knowledge-catalog.md"
      )
    }
    subagents = [PSCustomObject]@{
      mode = "guided"
      allowed_roles = @(
        "explorer",
        "readonly-research",
        "docs-researcher",
        "planner",
        "architect",
        "reviewer",
        "stage1-reviewer",
        "stage2-reviewer",
        "security-reviewer",
        "test-planner",
        "failure-triage",
        "visual-reviewer",
        "harness-writer",
        "docs-worker",
        "frontend-worker",
        "backend-worker",
        "test-runner"
      )
      max_parallel_roles = 3
      notes = @(
        "Only enable subagents when the task has two or more independent subtasks, or when the controller/session must delegate a file write instead of writing directly.",
        "Subagents must read the matching AGENTS/rules/docs/skills before concluding.",
        "Read-only roles stay read-only and return summaries or concrete results, not full transcripts.",
        "Any repo write outside driver-owned runtime outputs must go to a matching writer role; the controller/main session should not write directly."
      )
    }
    git = [PSCustomObject]@{
      require_clean_workspace = $true
      non_blocking_dirty_paths = @()
    }
    codex = [PSCustomObject]@{
      implementation_sandbox = "danger-full-access"
      review_sandbox = "danger-full-access"
      disable_hooks = $false
    }
  }
}

function Merge-PolicySection {
  <#
    用覆盖对象更新默认策略对象。
  #>
  param(
    [object]$BaseSection,
    [object]$OverrideSection
  )

  $merged = [PSCustomObject]@{}
  foreach ($property in $BaseSection.PSObject.Properties) {
    $value = $property.Value
    if ($value -is [System.Array]) {
      $value = @($value)
    }
    Add-Member -InputObject $merged -MemberType NoteProperty -Name $property.Name -Value $value
  }

  if ($null -eq $OverrideSection) {
    return $merged
  }

  foreach ($property in $OverrideSection.PSObject.Properties) {
    $value = $property.Value
    if ($value -is [System.Array]) {
      $value = @($value)
    }

    if ($null -eq $merged.PSObject.Properties[$property.Name]) {
      Add-Member -InputObject $merged -MemberType NoteProperty -Name $property.Name -Value $value
    }
    else {
      $merged.$($property.Name) = $value
    }
  }

  return $merged
}

function Resolve-RunProfilePath {
  <#
    解析运行策略文件路径，优先级: 参数 > task.runtime.run_profile。
  #>
  param(
    [string]$Root,
    [string]$ConfiguredPath,
    [object]$TaskDocument
  )

  $runtime = Get-ObjectPropertyValue -InputObject $TaskDocument -Name "runtime"
  $runtimeProfile = Get-ObjectPropertyValue -InputObject $runtime -Name "run_profile"
  $candidate = $ConfiguredPath
  if ([string]::IsNullOrWhiteSpace($candidate)) {
    $candidate = $runtimeProfile
  }

  if ([string]::IsNullOrWhiteSpace($candidate)) {
    return $null
  }

  if ([System.IO.Path]::IsPathRooted($candidate)) {
    return $candidate
  }

  return Join-Path $Root $candidate
}

function Get-TaskExecutionPolicy {
  <#
    组合默认 profile、runtime 默认项和 task 覆盖项。
  #>
  param(
    [string]$Root,
    [string]$ConfiguredRunProfilePath,
    [object]$TaskDocument,
    [object]$Task
  )

  $defaultProfile = Get-DefaultRunProfile
  $resolvedProfilePath = Resolve-RunProfilePath -Root $Root -ConfiguredPath $ConfiguredRunProfilePath -TaskDocument $TaskDocument
  $profileOverrides = $null
  if ($null -ne $resolvedProfilePath -and (Test-Path -LiteralPath $resolvedProfilePath)) {
    $profileOverrides = Get-Content -LiteralPath $resolvedProfilePath -Raw | ConvertFrom-Json
  }

  $runtime = Get-ObjectPropertyValue -InputObject $TaskDocument -Name "runtime"
  $runtimeSession = Get-ObjectPropertyValue -InputObject $runtime -Name "session"
  $runtimeSubagents = Get-ObjectPropertyValue -InputObject $runtime -Name "subagents"
  $runtimeGit = Get-ObjectPropertyValue -InputObject $runtime -Name "git"
  $runtimeCodex = Get-ObjectPropertyValue -InputObject $runtime -Name "codex"
  $runtimeContentGates = Get-ObjectPropertyValue -InputObject $runtime -Name "content_gates"

  $taskExecution = Get-ObjectPropertyValue -InputObject $Task -Name "execution"
  $taskSession = Get-ObjectPropertyValue -InputObject $taskExecution -Name "session"
  $taskSubagents = Get-ObjectPropertyValue -InputObject $taskExecution -Name "subagents"
  $taskGit = Get-ObjectPropertyValue -InputObject $taskExecution -Name "git"
  $taskCodex = Get-ObjectPropertyValue -InputObject $taskExecution -Name "codex"
  $taskContentGates = Get-ObjectPropertyValue -InputObject $taskExecution -Name "content_gates"

  $session = Merge-PolicySection -BaseSection $defaultProfile.session -OverrideSection (Get-ObjectPropertyValue -InputObject $profileOverrides -Name "session")
  $session = Merge-PolicySection -BaseSection $session -OverrideSection $runtimeSession
  $session = Merge-PolicySection -BaseSection $session -OverrideSection $taskSession
  $session.context_sources = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $session -Name "context_sources")
  $contextSourceReferences = ConvertTo-HarnessDisplayReferences -Root $Root -RelativePaths $session.context_sources
  if ($null -ne $session.PSObject.Properties["context_source_references"]) {
    $session.context_source_references = $contextSourceReferences
  }
  else {
    Add-Member -InputObject $session -MemberType NoteProperty -Name "context_source_references" -Value $contextSourceReferences -Force
  }

  $subagents = Merge-PolicySection -BaseSection $defaultProfile.subagents -OverrideSection (Get-ObjectPropertyValue -InputObject $profileOverrides -Name "subagents")
  $subagents = Merge-PolicySection -BaseSection $subagents -OverrideSection $runtimeSubagents
  $subagents = Merge-PolicySection -BaseSection $subagents -OverrideSection $taskSubagents
  $subagents.allowed_roles = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $subagents -Name "allowed_roles")
  $subagents.notes = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $subagents -Name "notes")

  $gitPolicy = Merge-PolicySection -BaseSection $defaultProfile.git -OverrideSection (Get-ObjectPropertyValue -InputObject $profileOverrides -Name "git")
  $gitPolicy = Merge-PolicySection -BaseSection $gitPolicy -OverrideSection $runtimeGit
  $gitPolicy = Merge-PolicySection -BaseSection $gitPolicy -OverrideSection $taskGit
  $gitPolicy.non_blocking_dirty_paths = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $gitPolicy -Name "non_blocking_dirty_paths")

  $codexPolicy = Merge-PolicySection -BaseSection $defaultProfile.codex -OverrideSection (Get-ObjectPropertyValue -InputObject $profileOverrides -Name "codex")
  $codexPolicy = Merge-PolicySection -BaseSection $codexPolicy -OverrideSection $runtimeCodex
  $codexPolicy = Merge-PolicySection -BaseSection $codexPolicy -OverrideSection $taskCodex

  $contentGateDefaults = Get-DefaultContentGateSettings
  $contentGates = Merge-PolicySection -BaseSection $contentGateDefaults -OverrideSection $runtimeContentGates
  $contentGates = Merge-PolicySection -BaseSection $contentGates -OverrideSection $taskContentGates
  $runtimeContentGateMode = [string](Get-ObjectPropertyValue -InputObject $runtimeContentGates -Name "mode" -Default "")
  if ([string]::IsNullOrWhiteSpace($runtimeContentGateMode)) {
    $runtimeContentGateMode = [string](Get-ObjectPropertyValue -InputObject $runtimeContentGates -Name "default_mode" -Default "")
  }
  if (-not [string]::IsNullOrWhiteSpace($runtimeContentGateMode)) {
    $contentGates.mode = $runtimeContentGateMode
  }
  $taskContentGateMode = [string](Get-ObjectPropertyValue -InputObject $Task -Name "content_gate_mode" -Default "")
  if (-not [string]::IsNullOrWhiteSpace($taskContentGateMode)) {
    $contentGates.mode = $taskContentGateMode
  }

  $taskRiskTags = ConvertTo-LowerUniqueStringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "risk_tags")
  $contentGates.mode = Normalize-ContentGateMode -Mode ([string](Get-ObjectPropertyValue -InputObject $contentGates -Name "mode" -Default "core"))
  $contentGates.risk_tags = ConvertTo-LowerUniqueStringArray -Value @(
    (Get-ObjectPropertyValue -InputObject $contentGates -Name "risk_tags"),
    $taskRiskTags
  )

  $changeKind = [string](Get-ObjectPropertyValue -InputObject $Task -Name "change_kind" -Default "")
  if ($changeKind.Trim().ToLowerInvariant() -eq "refactor" -and $contentGates.risk_tags -notcontains "refactor") {
    $contentGates.risk_tags = @($contentGates.risk_tags + @("refactor"))
  }

  $contextFiles = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "context_files")
  $taskDefaults = Get-TaskDefinitionDefaults -Task $Task

  return [PSCustomObject]@{
    run_profile_path = $resolvedProfilePath
    session = $session
    subagents = $subagents
    git = $gitPolicy
    codex = $codexPolicy
    content_gates = $contentGates
    context_files = $contextFiles
    task_kind = $taskDefaults.task_kind
    gate_profile = $taskDefaults.gate_profile
    required_truth_sources = $taskDefaults.required_truth_sources
    produces_artifacts = $taskDefaults.produces_artifacts
    phase = $taskDefaults.phase
  }
}

function Invoke-NativeCommandQuiet {
  <#
    运行原生命令并捕获 stdout/stderr。
    PowerShell 5.1 会把原生命令 stderr 包装成 ErrorRecord；这里避免 warning 触发 Stop。
  #>
  param([scriptblock]$Script)

  $previousErrorActionPreference = $ErrorActionPreference
  $ErrorActionPreference = "Continue"
  try {
    $output = & $Script 2>&1
    $exitCode = $LASTEXITCODE
  }
  finally {
    $ErrorActionPreference = $previousErrorActionPreference
  }

  return [PSCustomObject]@{
    ExitCode = $exitCode
    Output = ($output -join "`n")
  }
}

function Test-CodexBlocked {
  <#
    只识别 Codex 内层按约定输出的固定 BLOCKED 行。
    不要用宽泛的 "BLOCKED" 匹配，否则英文 blocked/阻塞说明会误判。
  #>
  param([string]$Output)

  return $Output -cmatch "(?m)^\s*BLOCKED(?:\s+-\s+.*)?\s*$"
}

function Test-RecoverableCodexCompletion {
  <#
    识别可继续推进的 Codex 完成态：
    - 进程退出码异常或缺失
    - 但 last_message 已存在
    - 且没有触发固定 BLOCKED 标记
    这类情况交给后续测试和 review 闸门裁决，不在实现阶段直接打成 BLOCKED。
  #>
  param(
    [object]$ExitCode,
    [string]$Output,
    [string]$LastMessage
  )

  if (($null -ne $ExitCode) -and ([int]$ExitCode -eq 0)) {
    return $false
  }

  if ([string]::IsNullOrWhiteSpace($LastMessage)) {
    return $false
  }

  if (Test-CodexBlocked -Output $LastMessage) {
    return $false
  }

  return $true
}

function Get-TaskDocument {
  <#
    读取并解析任务文件。
  #>
  param([string]$Path)

  if (-not (Test-Path -LiteralPath $Path)) {
    throw "找不到任务文件: $Path"
  }

  return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}

function Select-NextTask {
  <#
    选择优先级最高且依赖已完成的未通过任务。
  #>
  param([object]$TaskDocument)

  $completedIds = @{}
  foreach ($task in $TaskDocument.tasks) {
    if ($task.passes -eq $true) {
      $completedIds[$task.id] = $true
    }
  }

  $candidates = @($TaskDocument.tasks |
    Where-Object { $_.passes -eq $false } |
    Sort-Object priority)

  foreach ($task in $candidates) {
    $missing = @($task.dependencies | Where-Object { -not $completedIds.ContainsKey($_) })
    if ($missing.Count -eq 0) {
      return $task
    }
  }

  return $null
}

function Assert-CleanGitWorkspace {
  <#
    启动前确认 Git 工作区干净，或仅包含允许忽略的非阻塞路径。
  #>
  param(
    [string]$Root,
    [string[]]$NonBlockingPaths = @()
  )

  $status = & git -c core.quotepath=false -C $Root status --short --untracked-files=all
  if ($LASTEXITCODE -ne 0) {
    throw "当前目录不是有效 Git 仓库: $Root"
  }

  if (-not $status) {
    return
  }

  $normalizedAllowedPaths = @()
  foreach ($allowedPath in @(ConvertTo-StringArray -Value $NonBlockingPaths)) {
    $normalized = $allowedPath.Trim()
    if ([string]::IsNullOrWhiteSpace($normalized)) {
      continue
    }

    $normalized = $normalized.Replace("/", "\")
    if ($normalized.StartsWith(".\")) {
      $normalized = $normalized.Substring(2)
    }

    $normalizedAllowedPaths += $normalized.TrimStart("\")
  }

  $disallowedEntries = @()
  foreach ($line in @($status)) {
    if ([string]::IsNullOrWhiteSpace($line)) {
      continue
    }

    $relativePath = if ($line.Length -ge 4) { $line.Substring(3).Trim() } else { $line.Trim() }
    if ([string]::IsNullOrWhiteSpace($relativePath)) {
      continue
    }

    $normalizedRelativePath = $relativePath.Replace("/", "\")
    $isAllowed = $false
    foreach ($allowedPath in $normalizedAllowedPaths) {
      if ([string]::IsNullOrWhiteSpace($allowedPath)) {
        continue
      }

      $directoryPrefix = $allowedPath.TrimEnd("\") + "\"
      if (($normalizedRelativePath -eq $allowedPath) -or $normalizedRelativePath.StartsWith($directoryPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
        $isAllowed = $true
        break
      }
    }

    if (-not $isAllowed) {
      $disallowedEntries += $line
    }
  }

  if ($disallowedEntries.Count -eq 0) {
    return
  }

  if ($status) {
    $autoCommitSkillPath = Join-Path $Root ".agents\skills\auto-commit\SKILL.md"
    Write-Output "BLOCKED - 需要人工介入"
    Write-Output ""
    Write-Output "当前任务: 未选择"
    Write-Output ""
    Write-Output "阻塞原因:"
    Write-Output "- Git 工作区启动时已有未提交改动。"
    if ($normalizedAllowedPaths.Count -gt 0) {
      Write-Output "- 当前改动不在 non_blocking_dirty_paths 白名单内。"
    }
    Write-Output ""
    Write-Output "检测到的阻塞路径:"
    foreach ($entry in $disallowedEntries) {
      Write-Output "- $entry"
    }
    Write-Output ""
    Write-Output "需要人工操作:"
    Write-Output "1. 提交、暂存或清理这些改动。"
    $nextStepNumber = 2
    if (Test-Path -LiteralPath $autoCommitSkillPath) {
      Write-Output "$nextStepNumber. 如果这些改动就是当前任务的收尾结果、且没有混入用户无关改动，读取 `.agents/skills/auto-commit/SKILL.md` 并执行 Auto Review and Commit 流程。"
      $nextStepNumber++
    }
    if ($normalizedAllowedPaths.Count -gt 0) {
      Write-Output "$nextStepNumber. 如果这些改动属于可忽略运行产物，请把路径加入 runtime.git.non_blocking_dirty_paths。"
      $nextStepNumber++
    }
    Write-Output "$nextStepNumber. 确认没有用户改动会被自动提交混入。"
    exit 2
  }
}

function Invoke-RuntimeDoctor {
  <#
    在任务执行前检查 runtime 入口、.codex 配置和 harness 深文档是否完整。
  #>
  param([string]$Root)

  $doctorPath = Join-Path $Root "tools\harness\doctor.ps1"
  if (-not (Test-Path -LiteralPath $doctorPath)) {
    throw "找不到 runtime doctor: $doctorPath"
  }

  Push-Location $Root
  try {
    $output = powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\harness\doctor.ps1 2>&1
    $exitCode = $LASTEXITCODE
    return [PSCustomObject]@{
      ExitCode = $exitCode
      Output = ($output -join "`n")
    }
  }
  finally {
    Pop-Location
  }
}

function Get-PromptTemplate {
  <#
    优先读取项目内 prompt 模板；不存在时回退到内置模板。
  #>
  param(
    [string]$Root,
    [string]$TemplateName,
    [string]$FallbackTemplate
  )

  $relativePath = Join-Path ".codex\prompts" $TemplateName
  $fullPath = Join-Path $Root $relativePath
  if (Test-Path -LiteralPath $fullPath) {
    return Get-TextFileContent -Path $fullPath
  }

  return $FallbackTemplate
}

function New-ImplementationPrompt {
  <#
    根据任务对象生成给 Codex 的单任务 prompt。
  #>
  param(
    [object]$Task,
    [object]$ExecutionPolicy,
    [string]$TaskSessionId
  )

  $steps = ($Task.steps | ForEach-Object { "- $_" }) -join "`n"
  $acceptance = ($Task.acceptance | ForEach-Object { "- $_" }) -join "`n"
  $dependencies = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "dependencies"))
  $contextSourcesText = ConvertTo-BulletLines -Items $ExecutionPolicy.session.context_source_references
  $contextFilesText = ConvertTo-BulletLines -Items $ExecutionPolicy.context_files -EmptyText "- 无显式任务文件列表；仅按需读取与当前任务直接相关的文件"
  $allowedRoles = @(ConvertTo-StringArray -Value $ExecutionPolicy.subagents.allowed_roles)
  $allowedRolesText = if ($allowedRoles.Count -eq 0) { "none" } else { $allowedRoles -join ", " }
  $subagentNotesText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value $ExecutionPolicy.subagents.notes)
  $requiredTruthSourcesText = ConvertTo-BulletLines -Items $ExecutionPolicy.required_truth_sources -EmptyText "- 无前置 truth source 要求"
  $truthSourceState = Get-TruthSourceState -Root $ProjectRoot -RequiredTruthSources $ExecutionPolicy.required_truth_sources
  $truthSourceStateText = Convert-TruthSourceStateToBulletLines -States $truthSourceState
  $packageRootFallbacksText = ConvertTo-BulletLines -Items (Get-ExternalDisplayReferences -Root $ProjectRoot -Items (
      @($ExecutionPolicy.context_files) +
      @($truthSourceState | ForEach-Object { ConvertTo-StringArray -Value $_.present_paths })
    )) -EmptyText "- 无 packageRoot 回退路径"
  $producesArtifactsText = ConvertTo-BulletLines -Items $ExecutionPolicy.produces_artifacts -EmptyText "- 无显式产物要求"
  $requirementIdsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "requirement_ids")) -EmptyText "- 未在 task 中显式声明；实现前应先从 TRACEABILITY_MATRIX 或验收示例中确认"
  $beneficiaryText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "beneficiary")) -EmptyText "- 未显式声明受益对象"
  $successMetricsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "success_metrics")) -EmptyText "- 未显式声明成功指标"
  $productSpecSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "product_spec_summary")
  $domainModelSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "domain_model_summary")
  $implementationPlanSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "implementation_plan_summary")
  $architectureConstraintsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "architecture_constraints")) -EmptyText "- 未在 task 中显式声明；如当前任务需要架构门禁，先从架构约束 truth source 确认"
  $forbiddenImplementationsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "forbidden_implementations")) -EmptyText "- 未在 task 中显式声明；遵守架构约束 truth source 与项目通用禁止事项"
  $qaContractJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "qa_contract")
  $fallbackTemplate = @"
你是 Codex 自动实现会话。只处理 Driver Context 中的一个任务。不要修改 task.json、progress.txt、traces/ 或执行 git 命令。先确认验收示例、追溯矩阵和测试影响，再实现最小必要改动；完成后总结修改内容、验证结果和剩余风险。
"@
  $template = Get-PromptTemplate -Root $ProjectRoot -TemplateName "implement-one-task.md" -FallbackTemplate $fallbackTemplate

  return @"
$template

## Driver Context

任务 ID: $($Task.id)
任务标题: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "title" -Default ""))
任务描述: $($Task.description)
任务类型: $($ExecutionPolicy.task_kind)
Gate Profile: $($ExecutionPolicy.gate_profile)
Phase: $(if ([string]::IsNullOrWhiteSpace($ExecutionPolicy.phase)) { "未指定" } else { $ExecutionPolicy.phase })
优先级: $($Task.priority)
业务目标: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_goal" -Default ""))
问题定义: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "problem_statement" -Default ""))
受益对象:
$beneficiaryText
成功指标:
$successMetricsText
不做影响: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "not_doing_impact" -Default ""))
业务优先级: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority" -Default ""))
业务优先级原因: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority_reason" -Default ""))
产品规格摘要:
```json
$productSpecSummaryJson
```
领域模型摘要:
```json
$domainModelSummaryJson
```
Requirement IDs:
$requirementIdsText
依赖: $(if ($dependencies.Count -eq 0) { "none" } else { $dependencies -join ", " })
Task Session ID: $TaskSessionId
高风险协议:
- human_approval_required: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "human_approval_required" -Default ""))
- rollback_plan_required: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "rollback_plan_required" -Default ""))
- security_review_required: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "security_review_required" -Default ""))
- migration_review_required: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "migration_review_required" -Default ""))

会话策略:
- Session mode: $($ExecutionPolicy.session.mode)
- Reuse previous chat: $($ExecutionPolicy.session.reuse_previous_chat)
- Carry forward summary: $($ExecutionPolicy.session.carry_forward_summary)

优先上下文源:
$contextSourcesText

任务相关文件:
$contextFilesText

前置 truth source:
$requiredTruthSourcesText

truth source 完整性:
$truthSourceStateText

packageRoot 回退路径:
$packageRootFallbacksText

架构约束:
$architectureConstraintsText

禁止实现:
$forbiddenImplementationsText

QA Contract:
```json
$qaContractJson
```

Implementation Plan Contract:
```json
$implementationPlanSummaryJson
```

知识使用:
- 如果存在 `docs/knowledge/knowledge-catalog.md`，先按任务阶段判断是否需要读取 `docs/knowledge/catalog.md` 和相关条目。
- 如果引用了知识条目，最终总结必须列出 `knowledge_references`，包含 id、title、used_in 和 path。
- 如果发现可复用的新经验，最终总结必须列出 `knowledge_outputs` 建议，供 ARCHIVE 任务归档。

预期产物:
$producesArtifactsText

子代理策略:
- Mode: $($ExecutionPolicy.subagents.mode)
- Allowed roles: $allowedRolesText
- Max parallel roles: $($ExecutionPolicy.subagents.max_parallel_roles)
- Notes:
$subagentNotesText

执行步骤:
$steps

验收标准:
$acceptance

测试命令:
$($Task.test_command)
"@
}

function Get-ExistingRelativePaths {
  <#
    返回工作区内实际存在的相对路径列表。
  #>
  param(
    [string]$Root,
    [string[]]$RelativePaths
  )

  $existing = @()
  foreach ($relativePath in $RelativePaths) {
    if ([string]::IsNullOrWhiteSpace($relativePath)) {
      continue
    }

    $fullPath = Resolve-HarnessProjectOrPackagePath -Root $Root -RelativePath $relativePath
    if (Test-Path -LiteralPath $fullPath) {
      $existing += Get-HarnessDisplayReference -Root $Root -RelativePath $relativePath
    }
  }

  return $existing
}

function Get-FirstExistingRelativePath {
  <#
    返回候选路径中第一个实际存在的相对路径。
  #>
  param(
    [string]$Root,
    [string[]]$Candidates
  )

  foreach ($candidate in @(ConvertTo-StringArray -Value $Candidates)) {
    $fullPath = Resolve-HarnessProjectOrPackagePath -Root $Root -RelativePath $candidate
    if (Test-Path -LiteralPath $fullPath) {
      return (Get-HarnessDisplayReference -Root $Root -RelativePath $candidate)
    }
  }

  return $null
}

function Get-ExistingDevPlans {
  <#
    收集存在的 DEV-PLAN 文件，避免让 reviewer 猜测计划入口。
  #>
  param([string]$Root)

  $plansRoot = Join-Path $Root "plans"
  if (-not (Test-Path -LiteralPath $plansRoot)) {
    return @()
  }

  $matches = Get-ChildItem -Path $plansRoot -Recurse -Filter "*.dev-plan.md" -File -ErrorAction SilentlyContinue |
    Where-Object { $_.Name -ne "_template.dev-plan.md" } |
    Select-Object -First 5

  $paths = @()
  foreach ($match in $matches) {
    $paths += $match.FullName.Substring($Root.Length).TrimStart("\", "/")
  }

  return $paths
}

function Get-TruthSourceState {
  <#
    按 truth source 类型收集已满足和缺失的真相源。
  #>
  param(
    [string]$Root,
    [string[]]$RequiredTruthSources
  )

  $states = @()
  foreach ($truthSource in @(ConvertTo-UniqueStringArray -Items $RequiredTruthSources)) {
    switch ($truthSource) {
      "repo_context" {
        $presentPaths = @()
        $missingRequirements = @()

        $featurePack = Get-FirstExistingRelativePath -Root $Root -Candidates @(
          "feature-pack.md",
          "context\feature-pack.md",
          "docs\context\feature-pack.md"
        )
        if ($null -ne $featurePack) { $presentPaths += $featurePack } else { $missingRequirements += "feature-pack.md | context\\feature-pack.md | docs\\context\\feature-pack.md" }

        $repoMap = Get-FirstExistingRelativePath -Root $Root -Candidates @(
          "repo-map.md",
          "context\repo-map.md",
          "docs\context\repo-map.md",
          "docs\ai\repo-map.md"
        )
        if ($null -ne $repoMap) { $presentPaths += $repoMap } else { $missingRequirements += "repo-map.md | context\\repo-map.md | docs\\context\\repo-map.md | docs\\ai\\repo-map.md" }

        $architectureBrief = Get-FirstExistingRelativePath -Root $Root -Candidates @(
          "architecture-brief.md",
          "context\architecture-brief.md",
          "docs\context\architecture-brief.md",
          "docs\ai\architecture-brief.md",
          "docs\architecture\architecture-brief.md"
        )
        if ($null -ne $architectureBrief) { $presentPaths += $architectureBrief } else { $missingRequirements += "architecture-brief.md | context\\architecture-brief.md | docs\\context\\architecture-brief.md | docs\\ai\\architecture-brief.md | docs\\architecture\\architecture-brief.md" }

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "research" {
        $presentPaths = @()
        $researchRoot = Join-Path $Root "docs\research"
        if (Test-Path -LiteralPath $researchRoot) {
          $presentPaths += @(Get-ChildItem -Path $researchRoot -Recurse -Filter "*.md" -File -ErrorAction SilentlyContinue |
            ForEach-Object { $_.FullName.Substring($Root.Length).TrimStart("\", "/") })
        }

        $adrRoot = Join-Path $Root "docs\adr"
        if (Test-Path -LiteralPath $adrRoot) {
          $presentPaths += @(Get-ChildItem -Path $adrRoot -Filter "ADR-*.md" -File -ErrorAction SilentlyContinue |
            ForEach-Object { $_.FullName.Substring($Root.Length).TrimStart("\", "/") })
        }

        $presentPaths = ConvertTo-UniqueStringArray -Items $presentPaths
        $missingRequirements = @()
        if ($presentPaths.Count -eq 0) {
          $missingRequirements += "docs\\research\\*.md | docs\\adr\\ADR-*.md"
        }

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "product" {
        $requiredPaths = @(
          "docs\product\prd-lite.md",
          "docs\product\page-inventory.md",
          "docs\product\state-matrix.yaml",
          "docs\product\acceptance-criteria.md",
          "docs\product\requirement-interface-matrix.md",
          "docs\product\difficulty-research.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $requiredPaths
        $missingRequirements = @($requiredPaths | Where-Object { $presentPaths -notcontains $_ })

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "design" {
        $requiredPaths = @(
          "docs\design\design-brief.md",
          "docs\design\component-map.md",
          "docs\design\screen-states.md",
          "docs\design\design-tokens.json",
          "docs\design\ai-image-brief.md",
          "docs\design\ui-image-review.md",
          "docs\design\image-to-frontend-spec.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $requiredPaths
        $missingRequirements = @($requiredPaths | Where-Object { $presentPaths -notcontains $_ })

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "plan" {
        $presentPaths = Get-ExistingDevPlans -Root $Root
        $missingRequirements = @()
        if (@($presentPaths).Count -eq 0) {
          $missingRequirements += "plans\\**\\*.dev-plan.md"
        }

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "architecture" {
        $requiredPaths = @(
          "docs\architecture\constraints.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $requiredPaths
        $missingRequirements = @($requiredPaths | Where-Object { $presentPaths -notcontains $_ })

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "testing" {
        $requiredPaths = @(
          "docs\testing\ACCEPTANCE_CRITERIA.md",
          "docs\testing\ACCEPTANCE_EXAMPLES.md",
          "docs\testing\NATURAL_LANGUAGE_TEST_CASES.md",
          "docs\testing\TRACEABILITY_MATRIX.md",
          "docs\testing\TEST_DATA_MATRIX.md",
          "docs\testing\test-matrix.md",
          "docs\testing\verify-matrix.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $requiredPaths
        $missingRequirements = @($requiredPaths | Where-Object { $presentPaths -notcontains $_ })

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "contract" {
        $candidatePaths = @(
          "contracts\openapi.yaml",
          "docs\harness\install-manifest-governance.md",
          "docs\agent-package-redesign\migration-plan.md",
          "docs\agent-package-redesign\target-layering-standard.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $candidatePaths
        $missingRequirements = @()
        if (@($presentPaths).Count -eq 0) {
          $missingRequirements += "contracts\\openapi.yaml | docs\\harness\\install-manifest-governance.md | docs\\agent-package-redesign\\migration-plan.md | docs\\agent-package-redesign\\target-layering-standard.md"
        }

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      "knowledge" {
        $requiredPaths = @(
          "docs\knowledge\knowledge-catalog.md",
          "docs\knowledge\catalog.md"
        )
        $presentPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths $requiredPaths
        $missingRequirements = @($requiredPaths | Where-Object { $presentPaths -notcontains $_ })

        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = ($missingRequirements.Count -eq 0)
          present_paths = $presentPaths
          missing_requirements = $missingRequirements
        }
      }
      default {
        $states += [PSCustomObject]@{
          source = $truthSource
          satisfied = $false
          present_paths = @()
          missing_requirements = @("unsupported truth source")
        }
      }
    }
  }

  return $states
}

function Convert-TruthSourceStateToBulletLines {
  <#
    将 truth source 满足情况格式化成多行 bullet。
  #>
  param([object[]]$States)

  if ($null -eq $States -or $States.Count -eq 0) {
    return "- 无前置 truth source 要求"
  }

  $lines = @()
  foreach ($state in $States) {
    $status = if ($state.satisfied) { "OK" } else { "MISSING" }
    $presentPaths = @($state.present_paths)
    $missingRequirements = @($state.missing_requirements)
    $presentText = if ($presentPaths.Count -gt 0) { $presentPaths -join ", " } else { "无" }
    $missingText = if ($missingRequirements.Count -gt 0) { $missingRequirements -join "; " } else { "无" }
    $lines += "- [$status] $($state.source): present=$presentText; missing=$missingText"
  }

  return $lines -join "`n"
}

function Get-ReviewPromptTemplate {
  <#
    优先读取项目内 prompt 模板；不存在时回退到内置模板。
  #>
  param(
    [string]$Root,
    [string]$TemplateName,
    [string]$FallbackTemplate
  )

  return Get-PromptTemplate -Root $Root -TemplateName $TemplateName -FallbackTemplate $FallbackTemplate
}

function Get-ReviewContextPaths {
  <#
    收集两阶段审查需要优先对照的真相源文件。
  #>
  param(
    [string]$Root,
    [object]$ExecutionPolicy
  )

  $productPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths @(
    "docs\product\prd-lite.md",
    "docs\product\page-inventory.md",
    "docs\product\state-matrix.yaml",
    "docs\product\acceptance-criteria.md",
    "docs\product\requirement-interface-matrix.md",
    "docs\product\difficulty-research.md"
  )

  $designPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths @(
    "docs\design\design-brief.md",
    "docs\design\component-map.md",
    "docs\design\screen-states.md",
    "docs\design\design-tokens.json",
    "docs\design\ai-image-brief.md",
    "docs\design\ui-image-review.md",
    "docs\design\image-to-frontend-spec.md"
  )

  $contractPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths @(
    "contracts\openapi.yaml"
  )
  $testingPaths = Get-ExistingRelativePaths -Root $Root -RelativePaths @(
    "docs\testing\ACCEPTANCE_CRITERIA.md",
    "docs\testing\ACCEPTANCE_EXAMPLES.md",
    "docs\testing\NATURAL_LANGUAGE_TEST_CASES.md",
    "docs\testing\TRACEABILITY_MATRIX.md",
    "docs\testing\TEST_STRATEGY.md",
    "docs\testing\RISK_BASED_TEST_PLAN.md",
    "docs\testing\TEST_DATA_MATRIX.md",
    "docs\testing\EVIDENCE_PROTOCOL.md",
    "docs\testing\REGRESSION_PLAN.md",
    "docs\testing\test-matrix.md",
    "docs\testing\test-data-plan.md",
    "docs\testing\verify-matrix.md",
    "docs\testing\failure-triage.md"
  )

  $planPaths = Get-ExistingDevPlans -Root $Root
  $contextFiles = Get-ExistingRelativePaths -Root $Root -RelativePaths $ExecutionPolicy.context_files
  $truthSourceState = Get-TruthSourceState -Root $Root -RequiredTruthSources $ExecutionPolicy.required_truth_sources
  $packageRootFallbacks = @(
    Get-ExternalDisplayReferences -Root $Root -Items (
      @($contextFiles) +
      @($truthSourceState | ForEach-Object { ConvertTo-StringArray -Value $_.present_paths })
    )
  )

  return [PSCustomObject]@{
    product = $productPaths
    design = $designPaths
    contracts = $contractPaths
    testing = $testingPaths
    plans = $planPaths
    task_context = $contextFiles
    truth_source_state = $truthSourceState
    missing_truth_sources = @($truthSourceState | Where-Object { -not $_.satisfied } | ForEach-Object { $_.source })
    package_root_fallbacks = $packageRootFallbacks
  }
}

function Get-ReviewVerdict {
  <#
    从 reviewer 输出中提取结构化 verdict，未显式给出时返回 UNKNOWN。
  #>
  param([string]$Output)

  $verdictPattern = 'PASS|FAIL|NEEDS_REPAIR|BLOCKED|HUMAN_REVIEW_REQUIRED'
  $sectionMatches = [regex]::Matches($Output, '(?ims)^\s*#{1,6}\s*(?:Final\s+)?Verdict\s*$([\s\S]*?)(?=^\s*#{1,6}\s+\S|\z)')
  if ($sectionMatches.Count -gt 0) {
    $lastSection = $sectionMatches[$sectionMatches.Count - 1].Groups[1].Value
    $sectionVerdict = [regex]::Match($lastSection, '(?im)^\s*(?:[-*]\s*)?(?:Verdict\s*[:：]\s*)?(' + $verdictPattern + ')\b')
    if ($sectionVerdict.Success) {
      return $sectionVerdict.Groups[1].Value.ToUpperInvariant()
    }
  }

  $matches = [regex]::Matches($Output, '(?im)^\s*(?:[-*]\s*)?(?:Final\s+Verdict|Verdict)\s*[:：]\s*(' + $verdictPattern + ')\b|^\s*[-*]\s*(' + $verdictPattern + ')\b')
  if ($matches.Count -gt 0) {
    $lastMatch = $matches[$matches.Count - 1]
    foreach ($group in $lastMatch.Groups) {
      if ($group.Value -in @("PASS", "FAIL", "NEEDS_REPAIR", "BLOCKED", "HUMAN_REVIEW_REQUIRED")) {
        return $group.Value.ToUpperInvariant()
      }
    }
  }

  return "UNKNOWN"
}

function Get-ReviewTerminalStatus {
  <#
    把 reviewer verdict 映射成 driver 使用的状态语义。
  #>
  param([string]$Verdict)

  switch (($Verdict ?? "").Trim().ToUpperInvariant()) {
    "PASS" { return "passed" }
    "BLOCKED" { return "blocked" }
    "HUMAN_REVIEW_REQUIRED" { return "blocked" }
    "NEEDS_REPAIR" { return "failed" }
    "FAIL" { return "failed" }
    default { return "failed" }
  }
}

function Get-ReviewFailureNote {
  param(
    [string]$StageName,
    [string]$Verdict
  )

  switch (($Verdict ?? "").Trim().ToUpperInvariant()) {
    "BLOCKED" { return "$StageName 判定为 BLOCKED，请先解除阻塞后再继续。" }
    "HUMAN_REVIEW_REQUIRED" { return "$StageName 判定为 HUMAN_REVIEW_REQUIRED，请人工确认高风险问题后再继续。" }
    "NEEDS_REPAIR" { return "$StageName 判定为 NEEDS_REPAIR，请先修复 findings 后再继续。" }
    default { return "$StageName 审查未通过，请先修复 findings 后再继续。" }
  }
}

function Get-EvidenceGateFindings {
  <#
    在 Set-TaskPassed 前检查实现总结中的关键证据段落是否齐全。
  #>
  param(
    [object]$Task,
    [string]$ImplementationOutput
  )

  $requiredSections = @(
    "Requirement IDs",
    "Summary",
    "Files Changed",
    "Validation",
    "Evidence",
    "Truth Sources Used",
    "Context Files Used",
    "Remaining Risks"
  )

  if ([bool](Get-ObjectPropertyValue -InputObject $Task -Name "test_strategy_required" -Default $false)) {
    $requiredSections += "Test Plan Summary"
  }

  $qaContract = Get-ObjectPropertyValue -InputObject $Task -Name "qa_contract" -Default $null
  if ($null -ne $qaContract) {
    $tddContract = Get-ObjectPropertyValue -InputObject $qaContract -Name "tdd_contract" -Default $null
    $tddPolicy = [string](Get-ObjectPropertyValue -InputObject $tddContract -Name "policy" -Default "")
    if ($tddPolicy.Trim().ToLowerInvariant() -eq "required") {
      $requiredSections += "TDD Evidence"
    }
  }

  $taskKind = [string](Get-ObjectPropertyValue -InputObject $Task -Name "task_kind" -Default "")
  if ($taskKind -in @("feature_impl", "release")) {
    $requiredSections += "Full Chain Validation"
  }

  $findings = @()
  foreach ($sectionName in @(ConvertTo-UniqueStringArray -Items $requiredSections)) {
    $sectionBody = Get-MarkdownSectionBody -Text $ImplementationOutput -SectionName $sectionName
    if ([string]::IsNullOrWhiteSpace($sectionBody)) {
      $findings += "missing evidence section: $sectionName"
    }
  }

  return $findings
}

function ConvertTo-RepairQueueFromFindings {
  param([string[]]$Findings)

  return @(
    @(ConvertTo-StringArray -Value $Findings) |
      Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
      ForEach-Object { "repair required: $_" }
  )
}

function New-Stage1ReviewPrompt {
  <#
    生成 Stage 1：Spec / 设计一致性审查 prompt。
  #>
  param(
    [object]$Task,
    [string]$Root,
    [object]$ExecutionPolicy,
    [object]$ReviewContext,
    [string]$ImplementationOutput = ""
  )

  $fallbackTemplate = @"
你是 Stage 1 Reviewer。只审查当前实现是否符合 Product Spec / Design Brief / 设计稿 / DEV-PLAN，不要讨论代码风格。
"@

  $template = Get-ReviewPromptTemplate -Root $Root -TemplateName "review-stage1-spec.md" -FallbackTemplate $fallbackTemplate
  $productText = ConvertTo-BulletLines -Items $ReviewContext.product
  $designText = ConvertTo-BulletLines -Items $ReviewContext.design
  $testingText = ConvertTo-BulletLines -Items $ReviewContext.testing
  $planText = ConvertTo-BulletLines -Items $ReviewContext.plans
  $contractText = ConvertTo-BulletLines -Items $ReviewContext.contracts
  $contextFilesText = ConvertTo-BulletLines -Items $ReviewContext.task_context
  $truthSourceStateText = Convert-TruthSourceStateToBulletLines -States $ReviewContext.truth_source_state
  $usageSummary = Get-TruthSourceUsageSummary -Root $Root -ReviewContext $ReviewContext -FinalMessage $ImplementationOutput -RequiredTruthSources $ExecutionPolicy.required_truth_sources
  $truthSourcesUsedText = ConvertTo-BulletLines -Items $usageSummary.truth_sources_used -EmptyText "- 实现总结未显式列出实际使用的 truth source"
  $contextFilesUsedText = ConvertTo-BulletLines -Items $usageSummary.context_files_used -EmptyText "- 实现总结未显式列出实际读取的 context files"
  $acceptanceText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value $Task.acceptance)
  $requirementIdsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "requirement_ids")) -EmptyText "- 未显式声明 Requirement IDs"
  $beneficiaryText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "beneficiary")) -EmptyText "- 未显式声明受益对象"
  $successMetricsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "success_metrics")) -EmptyText "- 未显式声明成功指标"
  $productSpecSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "product_spec_summary")
  $domainModelSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "domain_model_summary")
  $implementationPlanSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "implementation_plan_summary")
  $implementationPlanReported = Get-MarkdownSectionBody -Text $ImplementationOutput -SectionName "Implementation Plan Summary"
  if ([string]::IsNullOrWhiteSpace($implementationPlanReported)) {
    $implementationPlanReported = "- 实现总结未显式列出 Implementation Plan Summary"
  }

  return @"
$template

## Driver Context

- Task ID: $($Task.id)
- 描述: $($Task.description)
- Task Kind: $($ExecutionPolicy.task_kind)
- Gate Profile: $($ExecutionPolicy.gate_profile)
- 业务目标: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_goal" -Default ""))
- 问题定义: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "problem_statement" -Default ""))
- 受益对象:
$beneficiaryText
- 成功指标:
$successMetricsText
- 不做影响: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "not_doing_impact" -Default ""))
- 业务优先级: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority" -Default ""))
- 业务优先级原因: $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority_reason" -Default ""))
- 产品规格摘要:
```json
$productSpecSummaryJson
```
- 领域模型摘要:
```json
$domainModelSummaryJson
```
- Requirement IDs:
$requirementIdsText
- 验收标准:
$acceptanceText

优先真相源:

### Product
$productText

### Design
$designText

### Testing
$testingText

### DEV-PLAN
$planText

### Contract
$contractText

### Task Context Files
$contextFilesText

### Truth Source Completeness
$truthSourceStateText

### Truth Sources Used By Implementation
$truthSourcesUsedText

### Context Files Used By Implementation
$contextFilesUsedText

### Implementation Plan Contract
```json
$implementationPlanSummaryJson
```

### Implementation Plan Reported By Implementation
$implementationPlanReported

审查要求:
- 只看功能完整性、状态覆盖、与设计和计划的一致性。
- 不要重写代码，不要实现新功能。
- 对 `feature_plan` / 文档契约任务，如果 Product / DEV-PLAN 为空，但 Task Context Files、Truth Source Completeness、Requirement IDs 和验收标准已经给出等价规格来源，不要仅因 Product / DEV-PLAN 为空判 FAIL。
- 如果 Gate Profile 不是 `lightweight`，缺少 required truth source 视为阻塞性问题。
- 如果 Gate Profile 是 `lightweight`，可以记录缺口，但不要把 driver 自己负责的状态文件当作失败项。

输出要求:
- 必须包含 `Verdict: PASS`、`Verdict: NEEDS_REPAIR` 或 `Verdict: BLOCKED` 之一。
- 只要存在阻塞性功能/状态/设计偏差，就输出 `Verdict: NEEDS_REPAIR` 或 `Verdict: BLOCKED`。
"@
}

function New-Stage2ReviewPrompt {
  <#
    生成 Stage 2：代码质量 / 测试风险审查 prompt。
  #>
  param(
    [object]$Task,
    [string]$Root,
    [object]$ExecutionPolicy,
    [object]$ReviewContext,
    [object]$TestResult,
    [string]$ImplementationOutput = "",
    [string[]]$ChangedFiles = @()
  )

  $fallbackTemplate = @"
你是 Stage 2 Reviewer。Stage 1 已通过。现在只审查代码质量、测试、风险和可维护性，不要重新做需求裁判。
"@

  $template = Get-ReviewPromptTemplate -Root $Root -TemplateName "review-stage2-quality.md" -FallbackTemplate $fallbackTemplate
  $contextFilesText = ConvertTo-BulletLines -Items $ReviewContext.task_context
  $contractText = ConvertTo-BulletLines -Items $ReviewContext.contracts
  $testingText = ConvertTo-BulletLines -Items $ReviewContext.testing
  $requirementIdsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "requirement_ids")) -EmptyText "- 未显式声明 Requirement IDs"
  $qaContractJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "qa_contract")
  $implementationPlanSummaryJson = ConvertTo-PromptJson -Value (Get-ObjectPropertyValue -InputObject $Task -Name "implementation_plan_summary")
  $implementationPlanReported = Get-MarkdownSectionBody -Text $ImplementationOutput -SectionName "Implementation Plan Summary"
  if ([string]::IsNullOrWhiteSpace($implementationPlanReported)) {
    $implementationPlanReported = "- 实现总结未显式列出 Implementation Plan Summary"
  }
  $changedFilesText = ConvertTo-BulletLines -Items $ChangedFiles -EmptyText "- 当前工作区未检测到 changed files"

  return @"
$template

## Driver Context

- Task ID: $($Task.id)
- 描述: $($Task.description)
- Requirement IDs:
$requirementIdsText
- 测试命令: $($Task.test_command)
- 测试退出码: $($TestResult.ExitCode)

任务相关文件:
$contextFilesText

Contract 真相源:
$contractText

Testing 真相源:
$testingText

QA Contract:
```json
$qaContractJson
```

Implementation Plan Contract:
```json
$implementationPlanSummaryJson
```

Implementation Plan Reported By Implementation:
$implementationPlanReported

Changed Files:
$changedFilesText

测试输出摘要:
$($TestResult.Output)

审查要求:
- 只讨论代码质量、测试覆盖、回归风险、安全风险和可维护性。
- 不要回到 Stage 1 的需求/设计裁判。

输出要求:
- 必须包含 `Verdict: PASS`、`Verdict: NEEDS_REPAIR`、`Verdict: BLOCKED` 或 `Verdict: HUMAN_REVIEW_REQUIRED` 之一。
- 只要存在阻塞性质量问题、关键测试缺口或高风险待人工确认项，就不要输出 `PASS`。
"@
}

function Invoke-CodexTask {
  <#
    调用 codex exec 运行单任务。
    原始输出写入日志文件，终端只显示 driver 摘要。
  #>
  param(
    [string]$Prompt,
    [string]$Root,
    [string]$Command,
    [string]$LogDirectory,
    [string]$TaskId,
    [string]$Sandbox = "danger-full-access",
    [string]$ActivityLabel = "Codex",
    [int]$HeartbeatSeconds = 20,
    [bool]$DisableHooks = $false,
    [switch]$CaptureJsonEvents
  )

  New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null

  $promptPath = Join-Path $LogDirectory "prompt.txt"
  $stdoutPath = Join-Path $LogDirectory "stdout.log"
  $stderrPath = Join-Path $LogDirectory "stderr.log"
  $lastMessagePath = Join-Path $LogDirectory "final.txt"

  Set-Content -LiteralPath $promptPath -Value $Prompt -Encoding UTF8

  $arguments = @(
    "exec",
    "--sandbox",
    $Sandbox,
    "--color",
    "never"
  )
  if ($DisableHooks) {
    $arguments += @(
      "--disable",
      "hooks"
    )
  }
  $arguments += @(
    "--output-last-message",
    $lastMessagePath,
    "-C",
    $Root,
    "-"
  )
  if ($CaptureJsonEvents) {
    $arguments += "--json"
  }

  $resolvedCommand = Resolve-CodexStartProcessPath -Command $Command

  $process = Start-Process `
    -FilePath $resolvedCommand `
    -ArgumentList $arguments `
    -WorkingDirectory $Root `
    -RedirectStandardInput $promptPath `
    -RedirectStandardOutput $stdoutPath `
    -RedirectStandardError $stderrPath `
    -NoNewWindow `
    -PassThru

  Write-Step "$ActivityLabel 已启动，PID=$($process.Id)，日志: $stdoutPath"

  $startedAt = Get-Date
  $lastHeartbeatAt = $startedAt.AddSeconds(-1 * $HeartbeatSeconds)
  while (-not $process.HasExited) {
    Start-Sleep -Seconds 2
    $process.Refresh()

    $now = Get-Date
    if (($now - $lastHeartbeatAt).TotalSeconds -lt $HeartbeatSeconds) {
      continue
    }

    $elapsed = [int]($now - $startedAt).TotalSeconds
    $preview = Get-CompactLogPreview -Path $stdoutPath
    if ([string]::IsNullOrWhiteSpace($preview)) {
      $preview = Get-CompactLogPreview -Path $stderrPath
    }

    if ([string]::IsNullOrWhiteSpace($preview)) {
      Write-Step "$ActivityLabel 仍在运行，已耗时 ${elapsed}s，日志: $stdoutPath"
    }
    else {
      Write-Step "$ActivityLabel 仍在运行，已耗时 ${elapsed}s，最近输出: $preview"
    }

    $lastHeartbeatAt = $now
  }

  $process.WaitForExit()

  $stdout = Get-TextFileContent -Path $stdoutPath
  $stderr = Get-TextFileContent -Path $stderrPath
  $lastMessage = Get-TextFileContent -Path $lastMessagePath
  $stdoutPath = Remove-EmptyTraceLogIfPresent -Path $stdoutPath
  $stderrPath = Remove-EmptyTraceLogIfPresent -Path $stderrPath
  $lastMessagePath = Remove-EmptyTraceLogIfPresent -Path $lastMessagePath
  $combinedOutput = @($stdout, $stderr, $lastMessage) -join "`n"
  $reviewOutput = if ([string]::IsNullOrWhiteSpace($lastMessage)) { $combinedOutput } else { $lastMessage }

  return [PSCustomObject]@{
    ExitCode = $process.ExitCode
    Output = $combinedOutput
    ReviewOutput = $reviewOutput
    LastMessage = $lastMessage
    StdoutLog = $stdoutPath
    StderrLog = $stderrPath
    PromptLog = $promptPath
    LastMessageLog = $lastMessagePath
    EventsLog = $(if ($CaptureJsonEvents) { $stdoutPath } else { $null })
  }
}

function Get-TaskLogDirectory {
  <#
    为当前 task session 创建持久日志目录。
  #>
  param(
    [string]$TraceRoot,
    [string]$TaskId,
    [string]$SessionStamp
  )

  $taskDirectory = Join-Path $TraceRoot "$TaskId-$SessionStamp"
  New-Item -ItemType Directory -Force -Path $taskDirectory | Out-Null
  return $taskDirectory
}

function Remove-TraceLogDirectoryIfEmpty {
  param([string]$Directory)

  if ([string]::IsNullOrWhiteSpace($Directory)) {
    return
  }

  if (-not (Test-Path -LiteralPath $Directory -PathType Container)) {
    return
  }

  $items = @(Get-ChildItem -LiteralPath $Directory -Force -ErrorAction SilentlyContinue)
  if ($items.Count -eq 0) {
    Remove-Item -LiteralPath $Directory -Force -ErrorAction SilentlyContinue
  }
}

function Get-TraceLogFiles {
  <#
    汇总当前 task session 已产生的日志文件路径。
  #>
  param([object[]]$CodexResults)

  $paths = @()
  foreach ($result in @($CodexResults)) {
    if ($null -eq $result) {
      continue
    }

    $paths += ConvertTo-StringArray -Value @(
      $result.StdoutLog,
      $result.StderrLog,
      $result.PromptLog,
      $result.LastMessageLog
    )
  }

  return ConvertTo-UniqueStringArray -Items $paths
}

function Remove-EmptyTraceLogIfPresent {
  param([string]$Path)

  if ([string]::IsNullOrWhiteSpace($Path)) {
    return $null
  }

  if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
    return $null
  }

  $item = Get-Item -LiteralPath $Path
  if ($item.Length -gt 0) {
    return $Path
  }

  Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
  return $null
}

function Add-TraceDurableFields {
  <#
    为 trace payload 补齐 durable trace schema 字段。
  #>
  param(
    [object]$Trace,
    [string]$Root = "",
    [string]$SessionDirectory,
    [switch]$CaptureJsonEvents,
    [object]$EventsSource,
    [string[]]$LogFiles,
    [object]$Task = $null
  )

  if ($null -eq $Task) {
    $Task = Get-Variable -Name "CurrentTraceTask" -Scope Script -ValueOnly -ErrorAction SilentlyContinue
  }

  $qaContract = Get-ObjectPropertyValue -InputObject $Task -Name "qa_contract" -Default $null
  $acceptanceTestMatrix = @()
  $acceptanceVerification = @()
  if ($null -ne $qaContract) {
    $acceptanceTestMatrix = @(
      Get-ObjectPropertyValue -InputObject $qaContract -Name "acceptance_test_matrix" -Default @()
    )

    $caseGroups = @(
      @{ name = "positive"; items = @(Get-ObjectPropertyValue -InputObject $qaContract -Name "positive_cases" -Default @()) },
      @{ name = "negative"; items = @(Get-ObjectPropertyValue -InputObject $qaContract -Name "negative_cases" -Default @()) },
      @{ name = "boundary"; items = @(Get-ObjectPropertyValue -InputObject $qaContract -Name "boundary_cases" -Default @()) },
      @{ name = "regression"; items = @(Get-ObjectPropertyValue -InputObject $qaContract -Name "regression_cases" -Default @()) }
    )

    foreach ($caseGroup in $caseGroups) {
      foreach ($item in @($caseGroup.items)) {
        $acceptanceVerification += [PSCustomObject]@{
          acceptance = [string](Get-ObjectPropertyValue -InputObject $item -Name "acceptance" -Default "")
          case_type = [string]$caseGroup.name
          case_ids = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $item -Name "source_case_ids" -Default @()))
          verification = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $item -Name "verification" -Default @()))
          evidence_files = @()
          verdict = "UNKNOWN"
        }
      }
    }
  }

  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "schema_version" -Value "2.0.0" -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "session_dir" -Value $SessionDirectory -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "events_file" -Value $(if ($CaptureJsonEvents -and $null -ne $EventsSource) { $EventsSource.EventsLog } else { $null }) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "log_files" -Value @(ConvertTo-StringArray -Value $LogFiles) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "evidence_files" -Value @() -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "business_goal" -Value $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_goal" -Default "")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "problem_statement" -Value $([string](Get-ObjectPropertyValue -InputObject $Task -Name "problem_statement" -Default "")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "beneficiary" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "beneficiary")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "success_metrics" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "success_metrics")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "not_doing_impact" -Value $([string](Get-ObjectPropertyValue -InputObject $Task -Name "not_doing_impact" -Default "")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "business_priority" -Value $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority" -Default "")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "business_priority_reason" -Value $([string](Get-ObjectPropertyValue -InputObject $Task -Name "business_priority_reason" -Default "")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "product_spec_summary" -Value $(Get-ObjectPropertyValue -InputObject $Task -Name "product_spec_summary" -Default $null) -Force
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "product_spec_gaps")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "product_spec_gaps" -Value @() -Force
  }
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "domain_entities" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "domain_entities")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "state_model" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "state_model")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "business_rules" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "business_rules")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "invariants" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "invariants")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "domain_logic_locations" -Value @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "domain_logic_locations")) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "domain_model_summary" -Value $(Get-ObjectPropertyValue -InputObject $Task -Name "domain_model_summary" -Default $null) -Force
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "domain_model_gaps")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "domain_model_gaps" -Value @() -Force
  }
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "architecture_risks" -Value @(
    (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "architecture_constraints")) +
    (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "forbidden_implementations"))
  ) -Force
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "ownership_check")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "ownership_check" -Value ([PSCustomObject]@{
        status = "unknown"
        unexpected_paths = @()
        forbidden_paths = @()
      }) -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "dependency_changes")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "dependency_changes" -Value ([PSCustomObject]@{
        status = "unknown"
        changed_files = @()
        allowed_dependencies = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "allowed_dependencies"))
      }) -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "boundary_violations")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "boundary_violations" -Value @() -Force
  }
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "acceptance_matrix" -Value @($acceptanceTestMatrix) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "acceptance_verification" -Value @($acceptanceVerification) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "implementation_plan_summary" -Value $(Get-ObjectPropertyValue -InputObject $Task -Name "implementation_plan_summary" -Default $null) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "test_plan_summary" -Value $(Get-ObjectPropertyValue -InputObject $Task -Name "test_plan_summary" -Default $null) -Force
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "test_result_summary")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "test_result_summary" -Value $null -Force
  }
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "test_planner_used" -Value $([bool](Get-ObjectPropertyValue -InputObject $Task -Name "test_strategy_required" -Default $false)) -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "impact_analysis_summary" -Value $(Get-ObjectPropertyValue -InputObject $Task -Name "impact_analysis_summary" -Default $null) -Force
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "impact_summary")) {
    $impactSummaryObject = Get-ObjectPropertyValue -InputObject $Task -Name "impact_analysis_summary" -Default $null
    $impactSummaryText = ""
    if ($null -ne $impactSummaryObject) {
      $impactSummaryText = "relevant_files, call_chain, data_flow, state_flow, affected_apis, affected_tests, compatibility/security/performance/concurrency/migration/rollback/owned_paths recorded"
    }
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "impact_summary" -Value $(if ([string]::IsNullOrWhiteSpace($impactSummaryText)) { $null } else { $impactSummaryText }) -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "knowledge_references")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "knowledge_references" -Value @() -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "knowledge_outputs")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "knowledge_outputs" -Value @() -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "archive_summary")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "archive_summary" -Value $null -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "public_api_changes")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "public_api_changes" -Value @() -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "sensitive_data_findings")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "sensitive_data_findings" -Value @() -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "error_handling_findings")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "error_handling_findings" -Value @() -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "risk_review_summary")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "risk_review_summary" -Value $null -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "failure_triage_summary")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "failure_triage_summary" -Value $null -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "failure_triage_report")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "failure_triage_report" -Value $null -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "evidence_gate_status")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "evidence_gate_status" -Value "UNKNOWN" -Force
  }
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "repair_queue")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "repair_queue" -Value @() -Force
  }
  $taskDepthProtocol = Get-ObjectPropertyValue -InputObject $qaContract -Name "depth_protocol" -Default $null
  if ($null -eq (Get-ObjectPropertyValue -InputObject $Trace -Name "depth_protocol")) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "depth_protocol" -Value ([PSCustomObject]@{
        input_contract_check = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "input_contract_check" -Default $false)) { "required" } else { "not_required" })
        codebase_recon = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "codebase_recon" -Default $false)) { "required" } else { "not_required" })
        impact_analysis = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "impact_analysis" -Default $false)) { if ($null -ne (Get-ObjectPropertyValue -InputObject $Task -Name "impact_analysis_summary" -Default $null)) { "required_with_summary" } else { "required_missing_summary" } } else { "not_required" })
        implementation_plan = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "implementation_plan" -Default $false)) { if ($null -ne (Get-ObjectPropertyValue -InputObject $Task -Name "implementation_plan_summary" -Default $null)) { "required_with_summary" } else { "required_missing_summary" } } else { "not_required" })
        test_plan = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "test_plan" -Default $false)) { if ($null -ne (Get-ObjectPropertyValue -InputObject $Task -Name "test_plan_summary" -Default $null)) { "required_with_summary" } else { "required_missing_summary" } } else { "not_required" })
        self_review = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "self_review" -Default $false)) { "required" } else { "not_required" })
        evidence_summary = $(if ($null -eq $taskDepthProtocol) { "unknown" } elseif ([bool](Get-ObjectPropertyValue -InputObject $taskDepthProtocol -Name "evidence_summary" -Default $false)) { "required" } else { "not_required" })
      }) -Force
  }
  $usageSourceText = [string](Get-ObjectPropertyValue -InputObject $Trace -Name "final_message" -Default "")
  $traceLogs = Get-ObjectPropertyValue -InputObject $Trace -Name "logs" -Default $null
  $implementationLastMessagePath = [string](Get-ObjectPropertyValue -InputObject $traceLogs -Name "implementation_last_message" -Default "")
  if (-not [string]::IsNullOrWhiteSpace($implementationLastMessagePath)) {
    $implementationLastMessage = Get-TextFileContent -Path $implementationLastMessagePath
    if (-not [string]::IsNullOrWhiteSpace($implementationLastMessage)) {
      $usageSourceText = $implementationLastMessage
    }
  }

  $publicApiChanges = @(Get-MarkdownSectionItems -Text $usageSourceText -SectionName "Public API Changes")
  $sensitiveDataFindings = @(Get-MarkdownSectionItems -Text $usageSourceText -SectionName "Sensitive Data Review")
  $errorHandlingFindings = @(Get-MarkdownSectionItems -Text $usageSourceText -SectionName "Error Handling Review")
  $riskReviewSummary = Get-MarkdownSectionBody -Text $usageSourceText -SectionName "Risk Review Summary"
  if (-not [string]::IsNullOrWhiteSpace($riskReviewSummary)) {
    Add-Member -InputObject $Trace -MemberType NoteProperty -Name "risk_review_summary" -Value $riskReviewSummary -Force
  }
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "public_api_changes" -Value $publicApiChanges -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "sensitive_data_findings" -Value $sensitiveDataFindings -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "error_handling_findings" -Value $errorHandlingFindings -Force

  $usageSummary = Get-TruthSourceUsageSummary `
    -Root $Root `
    -ReviewContext (Get-ObjectPropertyValue -InputObject $Trace -Name "review_context" -Default $null) `
    -FinalMessage $usageSourceText `
    -RequiredTruthSources @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Trace -Name "required_truth_sources"))
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "truth_sources_used" -Value $usageSummary.truth_sources_used -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "context_files_used" -Value $usageSummary.context_files_used -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "truth_source_gaps" -Value $usageSummary.truth_source_gaps -Force
  Add-Member -InputObject $Trace -MemberType NoteProperty -Name "package_root_fallbacks" -Value $usageSummary.package_root_fallbacks -Force
  return $Trace
}

function Invoke-TestCommand {
  <#
    执行任务指定的验证命令，并在需要时持久化测试输出。
  #>
  param(
    [string]$Command,
    [string]$Root,
    [string]$LogDirectory = "",
    [string]$TaskId = "task"
  )

  Push-Location $Root
  try {
    $previousErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = "Continue"
    try {
      $output = powershell -NoProfile -Command "& { `$ErrorActionPreference = 'Continue'; $Command }" 2>&1
    }
    finally {
      $ErrorActionPreference = $previousErrorActionPreference
    }
    $exitCode = $LASTEXITCODE
    $outputText = ($output -join "`n")
    $outputLog = $null
    if (-not [string]::IsNullOrWhiteSpace($LogDirectory)) {
      New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
      $safeTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"
      $outputLog = Join-Path $LogDirectory "$TaskId-test-$safeTimestamp.log"
      Set-Content -LiteralPath $outputLog -Value $outputText -Encoding UTF8
    }

    return [PSCustomObject]@{
      ExitCode = $exitCode
      Output = $outputText
      OutputLog = $outputLog
    }
  }
  finally {
    Pop-Location
  }
}

function Invoke-SpecLintPrecheck {
  <#
    对需要 spec 真相源的任务执行内容级门禁，避免 TBD 规格直接进入实现闭环。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/spec-lint.ps1" -Label "spec"
}

function Test-ContentGateModeEnabled {
  <#
    core < recommended < full
  #>
  param(
    [object]$ExecutionPolicy,
    [string]$MinimumMode
  )

  $order = @{
    core = 1
    recommended = 2
    full = 3
  }

  $currentMode = Normalize-ContentGateMode -Mode ([string](Get-ObjectPropertyValue -InputObject $ExecutionPolicy.content_gates -Name "mode" -Default "core"))
  $requiredMode = Normalize-ContentGateMode -Mode $MinimumMode
  return $order[$currentMode] -ge $order[$requiredMode]
}

function Test-TaskHasRiskTag {
  param(
    [object]$ExecutionPolicy,
    [string[]]$Tags
  )

  $riskTags = ConvertTo-LowerUniqueStringArray -Value (Get-ObjectPropertyValue -InputObject $ExecutionPolicy.content_gates -Name "risk_tags")
  foreach ($tag in @(ConvertTo-LowerUniqueStringArray -Value $Tags)) {
    if ($riskTags -contains $tag) {
      return $true
    }
  }

  return $false
}

function Get-DefaultContentGateSettings {
  <#
    默认只启用 core 档位，避免新项目冷启动时被大量非关键门禁阻塞。
  #>
  return [PSCustomObject]@{
    mode = "core"
    risk_tags = @()
  }
}

function Normalize-ContentGateMode {
  param([string]$Mode)

  $value = if ([string]::IsNullOrWhiteSpace($Mode)) { "core" } else { $Mode.Trim().ToLowerInvariant() }
  if (@("core", "recommended", "full") -notcontains $value) {
    return "core"
  }

  return $value
}

function ConvertTo-LowerUniqueStringArray {
  param([object]$Value)

  $result = @()
  foreach ($item in @(ConvertTo-StringArray -Value $Value)) {
    $normalized = $item.Trim().ToLowerInvariant()
    if (-not [string]::IsNullOrWhiteSpace($normalized) -and $result -notcontains $normalized) {
      $result += $normalized
    }
  }

  return $result
}

function Get-MarkdownSectionItems {
  <#
    从 markdown 指定标题下提取 bullet / numbered lines，供 trace 记录实际使用的 truth source。
  #>
  param(
    [string]$Text,
    [string]$SectionName
  )

  if ([string]::IsNullOrWhiteSpace($Text) -or [string]::IsNullOrWhiteSpace($SectionName)) {
    return @()
  }

  $pattern = '(?ims)^\s*#{1,6}\s*' + [regex]::Escape($SectionName) + '\s*$([\s\S]*?)(?=^\s*#{1,6}\s+\S|\z)'
  $match = [regex]::Match($Text, $pattern)
  if (-not $match.Success) {
    return @()
  }

  $items = @()
  foreach ($line in ($match.Groups[1].Value -split "`r?`n")) {
    $trimmed = $line.Trim()
    if ($trimmed -match '^(?:[-*]|\d+\.)\s*(.+?)\s*$') {
      $items += $Matches[1].Trim()
    }
  }

  return @(ConvertTo-UniqueStringArray -Items $items)
}

function Get-MarkdownSectionBody {
  <#
    提取 markdown 指定标题下的原始正文，供 review / trace 消费。
  #>
  param(
    [string]$Text,
    [string]$SectionName
  )

  if ([string]::IsNullOrWhiteSpace($Text) -or [string]::IsNullOrWhiteSpace($SectionName)) {
    return ""
  }

  $pattern = '(?ims)^\s*#{1,6}\s*' + [regex]::Escape($SectionName) + '\s*$([\s\S]*?)(?=^\s*#{1,6}\s+\S|\z)'
  $match = [regex]::Match($Text, $pattern)
  if (-not $match.Success) {
    return ""
  }

  return $match.Groups[1].Value.Trim()
}

function Get-ExternalDisplayReferences {
  <#
    找出 display reference 中实际落到项目根外的路径，用于标记 packageRoot fallback。
  #>
  param(
    [string]$Root,
    [string[]]$Items
  )

  if ([string]::IsNullOrWhiteSpace($Root)) {
    return @()
  }

  $projectRootFull = ([System.IO.Path]::GetFullPath($Root)).TrimEnd('\', '/') + '\'
  $external = @()
  foreach ($item in @(ConvertTo-StringArray -Value $Items)) {
    if (-not [System.IO.Path]::IsPathRooted($item)) {
      continue
    }

    $itemFull = [System.IO.Path]::GetFullPath($item)
    if (-not $itemFull.StartsWith($projectRootFull, [System.StringComparison]::OrdinalIgnoreCase)) {
      $external += $itemFull
    }
  }

  return @(ConvertTo-UniqueStringArray -Items $external)
}

function Get-TruthSourceUsageSummary {
  <#
    从最终输出和 review context 汇总实际 truth source 使用证据与缺口。
  #>
  param(
    [string]$Root,
    [object]$ReviewContext,
    [string]$FinalMessage,
    [string[]]$RequiredTruthSources
  )

  $truthSourcesUsed = @(Get-MarkdownSectionItems -Text $FinalMessage -SectionName "Truth Sources Used")
  $contextFilesUsed = @(Get-MarkdownSectionItems -Text $FinalMessage -SectionName "Context Files Used")

  $truthSourceGaps = @()
  foreach ($state in @((Get-ObjectPropertyValue -InputObject $ReviewContext -Name "truth_source_state" -Default @()))) {
    if (-not [bool](Get-ObjectPropertyValue -InputObject $state -Name "satisfied" -Default $false)) {
      $truthSourceGaps += ("missing truth source: " + [string](Get-ObjectPropertyValue -InputObject $state -Name "source" -Default "unknown"))
    }
  }

  if (-not [string]::IsNullOrWhiteSpace($FinalMessage)) {
    if (@(ConvertTo-StringArray -Value $RequiredTruthSources).Count -gt 0 -and $truthSourcesUsed.Count -eq 0) {
      $truthSourceGaps += "final_message missing Truth Sources Used section"
    }

    $taskContext = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $ReviewContext -Name "task_context" -Default @()))
    if ($taskContext.Count -gt 0 -and $contextFilesUsed.Count -eq 0) {
      $truthSourceGaps += "final_message missing Context Files Used section"
    }
  }

  $fallbackCandidates = @()
  $fallbackCandidates += @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $ReviewContext -Name "task_context" -Default @()))
  foreach ($state in @((Get-ObjectPropertyValue -InputObject $ReviewContext -Name "truth_source_state" -Default @()))) {
    $fallbackCandidates += @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $state -Name "present_paths" -Default @()))
  }

  return [PSCustomObject]@{
    truth_sources_used = @(ConvertTo-UniqueStringArray -Items $truthSourcesUsed)
    context_files_used = @(ConvertTo-UniqueStringArray -Items $contextFilesUsed)
    truth_source_gaps = @(ConvertTo-UniqueStringArray -Items $truthSourceGaps)
    package_root_fallbacks = @(Get-ExternalDisplayReferences -Root $Root -Items $fallbackCandidates)
  }
}

function Invoke-HarnessLintPrecheck {
  <#
    统一执行 harness lint，支持 thin project 时回退到 packageRoot。
  #>
  param(
    [string]$Root,
    [string]$RelativePath,
    [string]$Label
  )

  $lintPath = Resolve-HarnessProjectOrPackagePath -Root $Root -RelativePath $RelativePath
  if (-not (Test-Path -LiteralPath $lintPath)) {
    throw "找不到 $Label lint: $lintPath"
  }

  Push-Location $Root
  try {
    $output = powershell -NoProfile -ExecutionPolicy Bypass -File $lintPath -ProjectRoot $Root 2>&1
    $exitCode = $LASTEXITCODE
    return [PSCustomObject]@{
      ExitCode = $exitCode
      Output = ($output -join "`n")
      ScriptPath = $lintPath
    }
  }
  finally {
    Pop-Location
  }
}

function Invoke-ContextLintPrecheck {
  <#
    对需要上下文导航的任务执行内容级门禁，避免在缺少项目导航索引时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/context-lint.ps1" -Label "context"
}

function Invoke-ArchitectureLintPrecheck {
  <#
    对需要架构门禁的任务执行轻量架构模板检查，避免缺少分层/复用/边界约束就进入实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/architecture-lint.ps1" -Label "architecture"
}

function Invoke-DirectoryLintPrecheck {
  <#
    对需要目录结构规则的任务执行轻量目录模板检查，避免在缺少目录规范时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/directory-lint.ps1" -Label "directory"
}

function Invoke-StyleLintPrecheck {
  <#
    对需要代码风格与命名规则的任务执行轻量模板检查，避免缺少业务词典和命名边界时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/style-lint.ps1" -Label "style"
}

function Invoke-ComponentLintPrecheck {
  <#
    对需要公共组件复用规则的任务执行轻量模板检查，避免没盘点组件和兼容影响就直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/component-lint.ps1" -Label "component"
}

function Invoke-BusinessLintPrecheck {
  <#
    对需要业务规则一致性的任务执行轻量模板检查，避免缺少业务规则、边界状态和异常路径约束就直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/business-lint.ps1" -Label "business"
}

function Invoke-ContractLintPrecheck {
  <#
    对需要接口与数据结构约束的任务执行轻量模板检查，避免缺少请求入口、响应包装和契约追溯时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/contract-lint.ps1" -Label "contract"
}

function Invoke-StateLintPrecheck {
  <#
    对需要状态管理一致性的任务执行轻量模板检查，避免缺少状态分层、唯一来源和缓存失效规则时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/state-lint.ps1" -Label "state"
}

function Invoke-UiLintPrecheck {
  <#
    对需要前端 UI 与交互质量约束的任务执行轻量模板检查，避免缺少设计系统、移动端和表单体验规则时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/ui-lint.ps1" -Label "ui"
}

function Invoke-BackendLintPrecheck {
  <#
    对需要后端服务分层质量约束的任务执行轻量模板检查，避免缺少 Controller/Service/Repository、事务和异常策略时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/backend-lint.ps1" -Label "backend"
}

function Invoke-SecurityLintPrecheck {
  <#
    对需要安全设计约束的任务执行轻量模板检查，避免缺少权限、输入校验、脱敏和风控策略时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/security-lint.ps1" -Label "security"
}

function Invoke-TestingLintPrecheck {
  <#
    对需要测试左移和验证完整性的任务执行轻量模板检查，避免缺少测试真相源时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/testing-lint.ps1" -Label "testing"
}

function Invoke-ImpactLintPrecheck {
  <#
    对需要影响面分析的任务执行轻量模板检查，避免缺少调用链、受影响对象和回归范围时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/impact-lint.ps1" -Label "impact"
}

function Invoke-PerformanceLintPrecheck {
  <#
    对需要性能预算与性能风险约束的任务执行轻量模板检查，避免缺少性能预算时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/performance-lint.ps1" -Label "performance"
}

function Invoke-ConfigLintPrecheck {
  <#
    对需要配置与环境约束的任务执行轻量模板检查，避免缺少环境变量、构建和部署说明时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/config-lint.ps1" -Label "config"
}

function Invoke-ObservabilityLintPrecheck {
  <#
    对需要日志与可观测性约束的任务执行轻量模板检查，避免缺少日志字段、审计和告警说明时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/observability-lint.ps1" -Label "observability"
}

function Invoke-RefactorLintPrecheck {
  <#
    对需要重构与维护约束的任务执行轻量模板检查，避免缺少任务类型、行为不变约束和死代码清理范围时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/refactor-lint.ps1" -Label "refactor"
}

function Invoke-SessionLintPrecheck {
  <#
    对需要多轮协作与交接约束的任务执行轻量模板检查，避免缺少会话交接字段和同文件隔离规则时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/session-lint.ps1" -Label "session"
}

function Invoke-DocsLintPrecheck {
  <#
    对需要文档与规范约束的任务执行轻量模板检查，避免核心文档集和文档更新字段缺失时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/docs-lint.ps1" -Label "docs"
}

function Invoke-DataLintPrecheck {
  <#
    对需要数据库与迁移约束的任务执行轻量模板检查，避免缺少 migration、历史数据和回滚口径时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/data-lint.ps1" -Label "data"
}

function Invoke-IntegrationLintPrecheck {
  <#
    对需要第三方服务集成约束的任务执行轻量模板检查，避免缺少回调、凭据和失败策略时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/integration-lint.ps1" -Label "integration"
}

function Invoke-MobileLintPrecheck {
  <#
    对需要移动端专项约束的任务执行轻量模板检查，避免缺少平台差异、权限和生命周期口径时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/mobile-lint.ps1" -Label "mobile"
}

function Invoke-AcceptanceLintPrecheck {
  <#
    对需要交付验收报告约束的任务执行轻量模板检查，避免缺少需求对照和回滚信息时直接实现。
  #>
  param([string]$Root)

  return Invoke-HarnessLintPrecheck -Root $Root -RelativePath "tools/harness/acceptance-lint.ps1" -Label "acceptance"
}

function Invoke-FailureTriage {
  <#
    在测试失败后运行只读失败归因链路，输出结构化 triage 结果。
  #>
  param(
    [object]$Task,
    [string]$Root,
    [object]$ExecutionPolicy,
    [object]$ReviewContext,
    [object]$CodexResult,
    [object]$Stage1Result,
    [object]$TestResult,
    [string]$CodexCommand,
    [string]$LogDirectory,
    [switch]$CaptureJsonEvents
  )

  $template = Get-PromptTemplate -Root $Root -TemplateName "failure-triage.md"
  $requirementIdsText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "requirement_ids")) -EmptyText "- 未显式声明 Requirement IDs"
  $acceptanceText = ConvertTo-BulletLines -Items (ConvertTo-StringArray -Value $Task.acceptance)
  $contextFilesText = ConvertTo-BulletLines -Items $ReviewContext.task_context
  $testingText = ConvertTo-BulletLines -Items $ReviewContext.testing
  $truthSourceStateText = Convert-TruthSourceStateToBulletLines -States $ReviewContext.truth_source_state
  $triageOutputPath = Join-Path $LogDirectory "$($Task.id)-failure-triage.json"
  $testOutputPathText = if ([string]::IsNullOrWhiteSpace($TestResult.OutputLog)) { "- 无测试日志文件" } else { "- $($TestResult.OutputLog)" }

  $prompt = @"
$template

## Driver Context

- Task ID: $($Task.id)
- 描述: $($Task.description)
- Requirement IDs:
$requirementIdsText
- 验收标准:
$acceptanceText
- 失败来源: test
- 测试命令: $($Task.test_command)
- 测试退出码: $($TestResult.ExitCode)

### Task Context Files
$contextFilesText

### Testing Truth Sources
$testingText

### Truth Source Completeness
$truthSourceStateText

### Evidence Files
- 实现 stdout: $($CodexResult.StdoutLog)
- 实现 stderr: $($CodexResult.StderrLog)
- Stage 1 stdout: $($Stage1Result.StdoutLog)
- Stage 1 stderr: $($Stage1Result.StderrLog)
$testOutputPathText

### Raw Test Output
```text
$($TestResult.Output)
```

输出要求:
- 只输出一个合法 JSON 对象。
- 必须包含 findings 数组；没有 finding 也要输出空数组。
- 优先把失败归类为 `TEST_CODE_ISSUE`、`PRODUCT_BUG`、`REQUIREMENT_CHANGE`、`ENV_OR_DATA_ISSUE` 或 `FLAKY`。
"@

  $triageResult = Invoke-CodexTask `
    -Prompt $prompt `
    -Root $Root `
    -Command $CodexCommand `
    -LogDirectory $LogDirectory `
    -TaskId "$($Task.id)-failure-triage" `
    -Sandbox $ExecutionPolicy.codex.review_sandbox `
    -ActivityLabel "Failure Triage" `
    -DisableHooks $ExecutionPolicy.codex.disable_hooks `
    -CaptureJsonEvents:$CaptureJsonEvents

  $triageJson = $triageResult.Output
  if ([string]::IsNullOrWhiteSpace($triageJson)) {
    $triageJson = $triageResult.LastMessage
  }

  $reportPath = $null
  $parseError = $null
  if (-not [string]::IsNullOrWhiteSpace($triageJson)) {
    try {
      $triageObject = $triageJson | ConvertFrom-Json -ErrorAction Stop
      $triageObject | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $triageOutputPath -Encoding UTF8
      $reportPath = $triageOutputPath
    }
    catch {
      $parseError = $_.Exception.Message
    }
  }

  return [PSCustomObject]@{
    ExitCode = $triageResult.ExitCode
    Output = $triageResult.Output
    LastMessage = $triageResult.LastMessage
    StdoutLog = $triageResult.StdoutLog
    StderrLog = $triageResult.StderrLog
    PromptLog = $triageResult.PromptLog
    LastMessageLog = $triageResult.LastMessageLog
    ReportPath = $reportPath
    ParseError = $parseError
  }
}

function Write-ProgressEntry {
  <#
    追加固定格式的人类可读进度记录。
  #>
  param(
    [string]$Path,
    [object]$Task,
    [string]$WorkSummary,
    [string]$TestSummary,
    [string]$Notes,
    [string]$Stage1Summary = "未运行",
    [string]$Stage2Summary = "未运行"
  )

  $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm"
  $entry = @"
## [$timestamp] - Task: $($Task.id) $($Task.description)

### 完成的工作：
- $WorkSummary

### 测试结果：
- $TestSummary

### 审查结果：
- Stage 1: $Stage1Summary
- Stage 2: $Stage2Summary

### 备注：
- $Notes
"@
  Add-Content -LiteralPath $Path -Value $entry
}

function Save-Trace {
  <#
    保存机器可读 trace。
  #>
  param(
    [string]$Directory,
    [object]$Trace
  )

  New-Item -ItemType Directory -Force -Path $Directory | Out-Null
  $safeTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"
  $path = Join-Path $Directory "$($Trace.task_id)-$safeTimestamp.json"
  $Trace | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8
  return $path
}

function Set-TaskPassed {
  <#
    将指定任务标记为通过并写回 JSON。
  #>
  param(
    [object]$TaskDocument,
    [string]$TaskId,
    [string]$Path
  )

  foreach ($task in $TaskDocument.tasks) {
    if ($task.id -eq $TaskId) {
      $task.passes = $true
    }
  }

  $TaskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Path -Encoding UTF8
}

function Get-TaskExecutionMode {
  <#
    返回任务执行模式。默认 single。
  #>
  param([object]$Task)

  $execution = Get-ObjectPropertyValue -InputObject $Task -Name "execution"
  $mode = Get-ObjectPropertyValue -InputObject $execution -Name "mode"
  if ([string]::IsNullOrWhiteSpace($mode)) {
    $mode = Get-ObjectPropertyValue -InputObject $Task -Name "mode" -Default "single"
  }

  return ([string]$mode).Trim().ToLowerInvariant()
}

function Invoke-OneTask {
  <#
    执行单个任务闭环。
  #>
  param(
    [string]$TaskPath,
    [string]$ProgressPath,
    [string]$TracePath
  )

  $startedAt = Get-Date
  $taskDocument = Get-TaskDocument -Path $TaskPath
  $task = Select-NextTask -TaskDocument $taskDocument

  if ($null -eq $task) {
    return [PSCustomObject]@{
      Status = "idle"
      TaskId = $null
    }
  }

  $executionPolicy = Get-TaskExecutionPolicy -Root $ProjectRoot -ConfiguredRunProfilePath $RunProfileFile -TaskDocument $taskDocument -Task $task
  $requireCleanWorkspace = Get-ObjectPropertyValue -InputObject $executionPolicy.git -Name "require_clean_workspace" -Default $true
  $nonBlockingDirtyPaths = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $executionPolicy.git -Name "non_blocking_dirty_paths")
  if ($requireCleanWorkspace) {
    Assert-CleanGitWorkspace -Root $ProjectRoot -NonBlockingPaths $nonBlockingDirtyPaths
  }

  $taskSessionId = [guid]::NewGuid().ToString("N")
  $taskSessionStamp = Get-Date -Format "yyyyMMdd-HHmmss"
  $taskLogDirectory = $null
  $taskMode = Get-TaskExecutionMode -Task $task
  if ($taskMode -ne "single") {
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未识别的执行模式。" -TestSummary "未运行测试。" -Notes "execution.mode = $taskMode。允许值: single。" -Stage1Summary "NOT_RUN - 未进入 Stage 1。" -Stage2Summary "NOT_RUN - 未进入 Stage 2。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = "unknown"
      agent = "codex-loop"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = "blocked"
      failed_stage = "execution_mode"
      stage1_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 1 审查。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 2 审查。"
      }
      blocked_reason = "未知 execution.mode: $taskMode"
      commands = @()
      logs = @{}
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $null -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "BLOCKED: 未知 execution.mode: $taskMode"
    return [PSCustomObject]@{
      Status = "blocked"
      TaskId = $task.id
      ExitCode = 2
    }
  }

  $initialReviewContext = Get-ReviewContextPaths -Root $ProjectRoot -ExecutionPolicy $executionPolicy
  if ($executionPolicy.gate_profile -ne "lightweight" -and $initialReviewContext.missing_truth_sources.Count -gt 0) {
    $truthSourceSummary = Convert-TruthSourceStateToBulletLines -States $initialReviewContext.truth_source_state
    $blockedReason = "缺少前置 truth sources: $($initialReviewContext.missing_truth_sources -join ', ')"
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，前置 truth source 检查未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$truthSourceSummary" -Stage1Summary "NOT_RUN - truth source precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = $executionPolicy.task_kind
      gate_profile = $executionPolicy.gate_profile
      required_truth_sources = $executionPolicy.required_truth_sources
      truth_source_state = $initialReviewContext.truth_source_state
      agent = "codex"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = "blocked"
      test_command = $task.test_command
      failed_stage = "truth_source_precheck"
      stage1_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "truth source precheck 未通过。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 2 审查。"
      }
      session_policy = $executionPolicy.session
      subagent_policy = $executionPolicy.subagents
      context_files = $executionPolicy.context_files
      run_profile_path = $executionPolicy.run_profile_path
      review_context = $initialReviewContext
      commands = @(@{ cmd = "truth_source_precheck"; exit_code = 1 })
      blocked_reason = $blockedReason
      final_message = $blockedReason
      logs = @{}
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $null -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "BLOCKED: 前置 truth source 检查未通过。"
    Write-Output $truthSourceSummary
    return [PSCustomObject]@{
      Status = "blocked"
      TaskId = $task.id
      ExitCode = 2
    }
  }

  $needsSpecLint = @("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsSpecLint) {
    $specLintResult = Invoke-SpecLintPrecheck -Root $ProjectRoot
    if ($specLintResult.ExitCode -ne 0) {
      $blockedReason = "spec 内容级门禁未通过，请先补齐需求范围、验收标准、边界与追溯。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，spec 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($specLintResult.Output)" -Stage1Summary "NOT_RUN - spec lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "spec_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "spec lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "spec_lint_precheck"; exit_code = $specLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          spec_lint = $specLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $null -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: spec 内容级门禁未通过。"
      Write-Output $specLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsContextLint = @("research_required", "spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsContextLint) {
    $contextLintResult = Invoke-ContextLintPrecheck -Root $ProjectRoot
    if ($contextLintResult.ExitCode -eq 1) {
      $blockedReason = "context 内容级门禁未通过，请先补齐 docs/ai 导航索引与当前任务上下文。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，context 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($contextLintResult.Output)" -Stage1Summary "NOT_RUN - context lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "context_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "context lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "context_lint_precheck"; exit_code = $contextLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          context_lint = $contextLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $null -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: context 内容级门禁未通过。"
      Write-Output $contextLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsSessionLint = @("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsSessionLint) {
    $sessionLintResult = Invoke-SessionLintPrecheck -Root $ProjectRoot
    if ($sessionLintResult.ExitCode -eq 1) {
      $blockedReason = "session 内容级门禁未通过，请先补齐 CURRENT_TASK 交接字段和多 Agent 同文件隔离规则。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，session 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($sessionLintResult.Output)" -Stage1Summary "NOT_RUN - session lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "session_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "session lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "session_lint_precheck"; exit_code = $sessionLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          session_lint = $sessionLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $null -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: session 内容级门禁未通过。"
      Write-Output $sessionLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsDocsLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsDocsLint) {
    $docsLintResult = Invoke-DocsLintPrecheck -Root $ProjectRoot
    if ($docsLintResult.ExitCode -eq 1) {
      $blockedReason = "docs 内容级门禁未通过，请先补齐核心文档集、文档分层入口和文档更新字段。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，docs 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($docsLintResult.Output)" -Stage1Summary "NOT_RUN - docs lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{ task_id = $task.id; task_session_id = $taskSessionId; task_kind = $executionPolicy.task_kind; gate_profile = $executionPolicy.gate_profile; required_truth_sources = $executionPolicy.required_truth_sources; agent = "codex"; started_at = $startedAt.ToString("o"); ended_at = (Get-Date).ToString("o"); status = "blocked"; test_command = $task.test_command; failed_stage = "docs_lint_precheck"; stage1_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "docs lint precheck 未通过。" }; stage2_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "未进入 Stage 2 审查。" }; session_policy = $executionPolicy.session; subagent_policy = $executionPolicy.subagents; context_files = $executionPolicy.context_files; run_profile_path = $executionPolicy.run_profile_path; review_context = $initialReviewContext; commands = @(@{ cmd = "docs_lint_precheck"; exit_code = $docsLintResult.ExitCode }); blocked_reason = $blockedReason; final_message = $blockedReason; logs = @{ docs_lint = $docsLintResult.Output } }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: docs 内容级门禁未通过。"
      Write-Output $docsLintResult.Output
      return [PSCustomObject]@{ Status = "blocked"; TaskId = $task.id; ExitCode = 2 }
    }
  }

  $needsDataLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("data", "database", "migration")
  if ($needsDataLint) {
    $dataLintResult = Invoke-DataLintPrecheck -Root $ProjectRoot
    if ($dataLintResult.ExitCode -eq 1) {
      $blockedReason = "data 内容级门禁未通过，请先补齐 migration、历史数据兼容和回滚策略。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，data 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($dataLintResult.Output)" -Stage1Summary "NOT_RUN - data lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{ task_id = $task.id; task_session_id = $taskSessionId; task_kind = $executionPolicy.task_kind; gate_profile = $executionPolicy.gate_profile; required_truth_sources = $executionPolicy.required_truth_sources; agent = "codex"; started_at = $startedAt.ToString("o"); ended_at = (Get-Date).ToString("o"); status = "blocked"; test_command = $task.test_command; failed_stage = "data_lint_precheck"; stage1_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "data lint precheck 未通过。" }; stage2_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "未进入 Stage 2 审查。" }; session_policy = $executionPolicy.session; subagent_policy = $executionPolicy.subagents; context_files = $executionPolicy.context_files; run_profile_path = $executionPolicy.run_profile_path; review_context = $initialReviewContext; commands = @(@{ cmd = "data_lint_precheck"; exit_code = $dataLintResult.ExitCode }); blocked_reason = $blockedReason; final_message = $blockedReason; logs = @{ data_lint = $dataLintResult.Output } }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: data 内容级门禁未通过。"
      Write-Output $dataLintResult.Output
      return [PSCustomObject]@{ Status = "blocked"; TaskId = $task.id; ExitCode = 2 }
    }
  }

  $needsIntegrationLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("integration", "third_party", "third-party", "external")
  if ($needsIntegrationLint) {
    $integrationLintResult = Invoke-IntegrationLintPrecheck -Root $ProjectRoot
    if ($integrationLintResult.ExitCode -eq 1) {
      $blockedReason = "integration 内容级门禁未通过，请先补齐第三方回调、凭据、重试和上传边界。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，integration 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($integrationLintResult.Output)" -Stage1Summary "NOT_RUN - integration lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{ task_id = $task.id; task_session_id = $taskSessionId; task_kind = $executionPolicy.task_kind; gate_profile = $executionPolicy.gate_profile; required_truth_sources = $executionPolicy.required_truth_sources; agent = "codex"; started_at = $startedAt.ToString("o"); ended_at = (Get-Date).ToString("o"); status = "blocked"; test_command = $task.test_command; failed_stage = "integration_lint_precheck"; stage1_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "integration lint precheck 未通过。" }; stage2_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "未进入 Stage 2 审查。" }; session_policy = $executionPolicy.session; subagent_policy = $executionPolicy.subagents; context_files = $executionPolicy.context_files; run_profile_path = $executionPolicy.run_profile_path; review_context = $initialReviewContext; commands = @(@{ cmd = "integration_lint_precheck"; exit_code = $integrationLintResult.ExitCode }); blocked_reason = $blockedReason; final_message = $blockedReason; logs = @{ integration_lint = $integrationLintResult.Output } }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: integration 内容级门禁未通过。"
      Write-Output $integrationLintResult.Output
      return [PSCustomObject]@{ Status = "blocked"; TaskId = $task.id; ExitCode = 2 }
    }
  }

  $needsMobileLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("mobile", "app", "ios", "android", "h5", "miniapp")
  if ($needsMobileLint) {
    $mobileLintResult = Invoke-MobileLintPrecheck -Root $ProjectRoot
    if ($mobileLintResult.ExitCode -eq 1) {
      $blockedReason = "mobile 内容级门禁未通过，请先补齐平台差异、权限弹窗、生命周期和真机矩阵。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，mobile 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($mobileLintResult.Output)" -Stage1Summary "NOT_RUN - mobile lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{ task_id = $task.id; task_session_id = $taskSessionId; task_kind = $executionPolicy.task_kind; gate_profile = $executionPolicy.gate_profile; required_truth_sources = $executionPolicy.required_truth_sources; agent = "codex"; started_at = $startedAt.ToString("o"); ended_at = (Get-Date).ToString("o"); status = "blocked"; test_command = $task.test_command; failed_stage = "mobile_lint_precheck"; stage1_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "mobile lint precheck 未通过。" }; stage2_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "未进入 Stage 2 审查。" }; session_policy = $executionPolicy.session; subagent_policy = $executionPolicy.subagents; context_files = $executionPolicy.context_files; run_profile_path = $executionPolicy.run_profile_path; review_context = $initialReviewContext; commands = @(@{ cmd = "mobile_lint_precheck"; exit_code = $mobileLintResult.ExitCode }); blocked_reason = $blockedReason; final_message = $blockedReason; logs = @{ mobile_lint = $mobileLintResult.Output } }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: mobile 内容级门禁未通过。"
      Write-Output $mobileLintResult.Output
      return [PSCustomObject]@{ Status = "blocked"; TaskId = $task.id; ExitCode = 2 }
    }
  }

  $needsAcceptanceLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsAcceptanceLint) {
    $acceptanceLintResult = Invoke-AcceptanceLintPrecheck -Root $ProjectRoot
    if ($acceptanceLintResult.ExitCode -eq 1) {
      $blockedReason = "acceptance 内容级门禁未通过，请先补齐验收报告、需求对照和回滚说明。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，acceptance 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($acceptanceLintResult.Output)" -Stage1Summary "NOT_RUN - acceptance lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{ task_id = $task.id; task_session_id = $taskSessionId; task_kind = $executionPolicy.task_kind; gate_profile = $executionPolicy.gate_profile; required_truth_sources = $executionPolicy.required_truth_sources; agent = "codex"; started_at = $startedAt.ToString("o"); ended_at = (Get-Date).ToString("o"); status = "blocked"; test_command = $task.test_command; failed_stage = "acceptance_lint_precheck"; stage1_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "acceptance lint precheck 未通过。" }; stage2_review = [PSCustomObject]@{ verdict = "NOT_RUN"; exit_code = $null; summary = "未进入 Stage 2 审查。" }; session_policy = $executionPolicy.session; subagent_policy = $executionPolicy.subagents; context_files = $executionPolicy.context_files; run_profile_path = $executionPolicy.run_profile_path; review_context = $initialReviewContext; commands = @(@{ cmd = "acceptance_lint_precheck"; exit_code = $acceptanceLintResult.ExitCode }); blocked_reason = $blockedReason; final_message = $blockedReason; logs = @{ acceptance_lint = $acceptanceLintResult.Output } }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: acceptance 内容级门禁未通过。"
      Write-Output $acceptanceLintResult.Output
      return [PSCustomObject]@{ Status = "blocked"; TaskId = $task.id; ExitCode = 2 }
    }
  }

  $needsArchitectureLint = @("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsArchitectureLint) {
    $architectureLintResult = Invoke-ArchitectureLintPrecheck -Root $ProjectRoot
    if ($architectureLintResult.ExitCode -eq 1) {
      $blockedReason = "architecture 内容级门禁未通过，请先补齐分层规则、复用检查和复杂功能技术方案字段。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，architecture 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($architectureLintResult.Output)" -Stage1Summary "NOT_RUN - architecture lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "architecture_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "architecture lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "architecture_lint_precheck"; exit_code = $architectureLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          architecture_lint = $architectureLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: architecture 内容级门禁未通过。"
      Write-Output $architectureLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsDirectoryLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsDirectoryLint) {
    $directoryLintResult = Invoke-DirectoryLintPrecheck -Root $ProjectRoot
    if ($directoryLintResult.ExitCode -eq 1) {
      $blockedReason = "directory 内容级门禁未通过，请先补齐目录规范、目录新增规则和本地规则索引。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，directory 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($directoryLintResult.Output)" -Stage1Summary "NOT_RUN - directory lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "directory_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "directory lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "directory_lint_precheck"; exit_code = $directoryLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          directory_lint = $directoryLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: directory 内容级门禁未通过。"
      Write-Output $directoryLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsStyleLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsStyleLint) {
    $styleLintResult = Invoke-StyleLintPrecheck -Root $ProjectRoot
    if ($styleLintResult.ExitCode -eq 1) {
      $blockedReason = "style 内容级门禁未通过，请先补齐业务词典、命名规则和类型边界。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，style 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($styleLintResult.Output)" -Stage1Summary "NOT_RUN - style lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "style_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "style lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "style_lint_precheck"; exit_code = $styleLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          style_lint = $styleLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: style 内容级门禁未通过。"
      Write-Output $styleLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsComponentLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsComponentLint) {
    $componentLintResult = Invoke-ComponentLintPrecheck -Root $ProjectRoot
    if ($componentLintResult.ExitCode -eq 1) {
      $blockedReason = "component 内容级门禁未通过，请先补齐组件清单、复用决策和公共组件兼容影响面。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，component 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($componentLintResult.Output)" -Stage1Summary "NOT_RUN - component lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "component_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "component lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "component_lint_precheck"; exit_code = $componentLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          component_lint = $componentLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: component 内容级门禁未通过。"
      Write-Output $componentLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsBusinessLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsBusinessLint) {
    $businessLintResult = Invoke-BusinessLintPrecheck -Root $ProjectRoot
    if ($businessLintResult.ExitCode -eq 1) {
      $blockedReason = "business 内容级门禁未通过，请先补齐业务规则、边界状态和异常路径测试约束。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，business 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($businessLintResult.Output)" -Stage1Summary "NOT_RUN - business lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "business_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "business lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "business_lint_precheck"; exit_code = $businessLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          business_lint = $businessLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: business 内容级门禁未通过。"
      Write-Output $businessLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsContractLint = @("contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsContractLint) {
    $contractLintResult = Invoke-ContractLintPrecheck -Root $ProjectRoot
    if ($contractLintResult.ExitCode -eq 1) {
      $blockedReason = "contract 内容级门禁未通过，请先补齐统一请求入口、响应包装、错误模型和契约追溯。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，contract 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($contractLintResult.Output)" -Stage1Summary "NOT_RUN - contract lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "contract_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "contract lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "contract_lint_precheck"; exit_code = $contractLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          contract_lint = $contractLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: contract 内容级门禁未通过。"
      Write-Output $contractLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsStateLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsStateLint) {
    $stateLintResult = Invoke-StateLintPrecheck -Root $ProjectRoot
    if ($stateLintResult.ExitCode -eq 1) {
      $blockedReason = "state 内容级门禁未通过，请先补齐状态分层、唯一状态来源和缓存失效策略。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，state 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($stateLintResult.Output)" -Stage1Summary "NOT_RUN - state lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "state_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "state lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "state_lint_precheck"; exit_code = $stateLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          state_lint = $stateLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: state 内容级门禁未通过。"
      Write-Output $stateLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsUiLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsUiLint) {
    $uiLintResult = Invoke-UiLintPrecheck -Root $ProjectRoot
    if ($uiLintResult.ExitCode -eq 1) {
      $blockedReason = "ui 内容级门禁未通过，请先补齐设计系统、移动端安全区、交互状态和表单体验规则。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，ui 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($uiLintResult.Output)" -Stage1Summary "NOT_RUN - ui lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "ui_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "ui lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "ui_lint_precheck"; exit_code = $uiLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          ui_lint = $uiLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: ui 内容级门禁未通过。"
      Write-Output $uiLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsBackendLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("backend", "api", "worker", "service")
  if ($needsBackendLint) {
    $backendLintResult = Invoke-BackendLintPrecheck -Root $ProjectRoot
    if ($backendLintResult.ExitCode -eq 1) {
      $blockedReason = "backend 内容级门禁未通过，请先补齐 Controller/Service/Repository 边界、事务/幂等和异常处理策略。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，backend 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($backendLintResult.Output)" -Stage1Summary "NOT_RUN - backend lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "backend_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "backend lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "backend_lint_precheck"; exit_code = $backendLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          backend_lint = $backendLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: backend 内容级门禁未通过。"
      Write-Output $backendLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsSecurityLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("security", "auth", "permission", "payment", "privacy")
  if ($needsSecurityLint) {
    $securityLintResult = Invoke-SecurityLintPrecheck -Root $ProjectRoot
    if ($securityLintResult.ExitCode -eq 1) {
      $blockedReason = "security 内容级门禁未通过，请先补齐权限校验、输入校验、脱敏和风控策略。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，security 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($securityLintResult.Output)" -Stage1Summary "NOT_RUN - security lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "security_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "security lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "security_lint_precheck"; exit_code = $securityLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          security_lint = $securityLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: security 内容级门禁未通过。"
      Write-Output $securityLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsTestingLint = @("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile
  if ($needsTestingLint) {
    $testingLintResult = Invoke-TestingLintPrecheck -Root $ProjectRoot
    if ($testingLintResult.ExitCode -eq 1) {
      $blockedReason = "testing 内容级门禁未通过，请先补齐验收、追溯、测试策略、验证矩阵和假测试约束。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，testing 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($testingLintResult.Output)" -Stage1Summary "NOT_RUN - testing lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "testing_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "testing lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "testing_lint_precheck"; exit_code = $testingLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          testing_lint = $testingLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: testing 内容级门禁未通过。"
      Write-Output $testingLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $taskDepth = [string](Get-ObjectPropertyValue -InputObject $task -Name "depth" -Default "")
  $impactAnalysisRequired = [bool](Get-ObjectPropertyValue -InputObject $task -Name "impact_analysis_required" -Default $false)
  $needsImpactLint = (
    @("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile -or
    @("deep", "critical") -contains $taskDepth -or
    $impactAnalysisRequired
  )
  if ($needsImpactLint) {
    $impactLintResult = Invoke-ImpactLintPrecheck -Root $ProjectRoot
    if ($impactLintResult.ExitCode -eq 1) {
      $blockedReason = "impact 内容级门禁未通过，请先补齐调用链、直接/间接影响和回归范围。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，impact 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($impactLintResult.Output)" -Stage1Summary "NOT_RUN - impact lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "impact_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "impact lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "impact_lint_precheck"; exit_code = $impactLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          impact_lint = $impactLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: impact 内容级门禁未通过。"
      Write-Output $impactLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsPerformanceLint = (Test-ContentGateModeEnabled -ExecutionPolicy $executionPolicy -MinimumMode "recommended") -and (@("spec_required", "contract_required", "release_required") -contains $executionPolicy.gate_profile)
  if ($needsPerformanceLint) {
    $performanceLintResult = Invoke-PerformanceLintPrecheck -Root $ProjectRoot
    if ($performanceLintResult.ExitCode -eq 1) {
      $blockedReason = "performance 内容级门禁未通过，请先补齐性能预算、复杂列表策略和后端性能约束。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，performance 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($performanceLintResult.Output)" -Stage1Summary "NOT_RUN - performance lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "performance_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "performance lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "performance_lint_precheck"; exit_code = $performanceLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          performance_lint = $performanceLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: performance 内容级门禁未通过。"
      Write-Output $performanceLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsConfigLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("config", "deploy", "env", "infra", "ci", "ops")
  if ($needsConfigLint) {
    $configLintResult = Invoke-ConfigLintPrecheck -Root $ProjectRoot
    if ($configLintResult.ExitCode -eq 1) {
      $blockedReason = "config 内容级门禁未通过，请先补齐环境变量、构建命令和部署影响说明。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，config 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($configLintResult.Output)" -Stage1Summary "NOT_RUN - config lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "config_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "config lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "config_lint_precheck"; exit_code = $configLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          config_lint = $configLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: config 内容级门禁未通过。"
      Write-Output $configLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsObservabilityLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("observability", "logging", "metrics", "trace", "alerting")
  if ($needsObservabilityLint) {
    $observabilityLintResult = Invoke-ObservabilityLintPrecheck -Root $ProjectRoot
    if ($observabilityLintResult.ExitCode -eq 1) {
      $blockedReason = "observability 内容级门禁未通过，请先补齐日志字段、trace 证据和告警策略。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，observability 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($observabilityLintResult.Output)" -Stage1Summary "NOT_RUN - observability lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "observability_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "observability lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "observability_lint_precheck"; exit_code = $observabilityLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          observability_lint = $observabilityLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: observability 内容级门禁未通过。"
      Write-Output $observabilityLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  $needsRefactorLint = Test-TaskHasRiskTag -ExecutionPolicy $executionPolicy -Tags @("refactor")
  if ($needsRefactorLint) {
    $refactorLintResult = Invoke-RefactorLintPrecheck -Root $ProjectRoot
    if ($refactorLintResult.ExitCode -eq 1) {
      $blockedReason = "refactor 内容级门禁未通过，请先补齐任务类型、行为不变约束和死代码清理范围。"
      Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "未进入实现阶段，refactor 内容级门禁未通过。" -TestSummary "未运行测试。" -Notes "$blockedReason`n$($refactorLintResult.Output)" -Stage1Summary "NOT_RUN - refactor lint precheck 未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
      $tracePayload = [PSCustomObject]@{
        task_id = $task.id
        task_session_id = $taskSessionId
        task_kind = $executionPolicy.task_kind
        gate_profile = $executionPolicy.gate_profile
        required_truth_sources = $executionPolicy.required_truth_sources
        agent = "codex"
        started_at = $startedAt.ToString("o")
        ended_at = (Get-Date).ToString("o")
        status = "blocked"
        test_command = $task.test_command
        failed_stage = "refactor_lint_precheck"
        stage1_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "refactor lint precheck 未通过。"
        }
        stage2_review = [PSCustomObject]@{
          verdict = "NOT_RUN"
          exit_code = $null
          summary = "未进入 Stage 2 审查。"
        }
        session_policy = $executionPolicy.session
        subagent_policy = $executionPolicy.subagents
        context_files = $executionPolicy.context_files
        run_profile_path = $executionPolicy.run_profile_path
        review_context = $initialReviewContext
        commands = @(@{ cmd = "refactor_lint_precheck"; exit_code = $refactorLintResult.ExitCode })
        blocked_reason = $blockedReason
        final_message = $blockedReason
        logs = @{
          refactor_lint = $refactorLintResult.Output
        }
      }
      Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $null -LogFiles @() | Out-Null
      Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
      Write-Step "BLOCKED: refactor 内容级门禁未通过。"
      Write-Output $refactorLintResult.Output
      return [PSCustomObject]@{
        Status = "blocked"
        TaskId = $task.id
        ExitCode = 2
      }
    }
  }

  Write-Step "选择任务: $($task.id) $($task.description)"
  Write-Step "启动新 task 会话: $taskSessionId ($($executionPolicy.session.mode))"
  $script:CurrentTraceTask = $task

  $prompt = New-ImplementationPrompt -Task $task -ExecutionPolicy $executionPolicy -TaskSessionId $taskSessionId
  $taskLogDirectory = Get-TaskLogDirectory -TraceRoot $TracePath -TaskId $task.id -SessionStamp $taskSessionStamp
  Write-Step "Codex 执行中，完整日志将写入 $taskLogDirectory"
  $codexResult = Invoke-CodexTask -Prompt $prompt -Root $ProjectRoot -Command $CodexCommand -LogDirectory $taskLogDirectory -TaskId $task.id -Sandbox $executionPolicy.codex.implementation_sandbox -ActivityLabel "实现阶段" -DisableHooks $executionPolicy.codex.disable_hooks -CaptureJsonEvents:$CaptureJsonEvents
  $codexBlocked = Test-CodexBlocked -Output $codexResult.LastMessage
  $codexRecoverableCompletion = Test-RecoverableCodexCompletion -ExitCode $codexResult.ExitCode -Output $codexResult.Output -LastMessage $codexResult.LastMessage

  if ($codexRecoverableCompletion -and $codexResult.ExitCode -ne 0) {
    Write-Step "检测到可继续推进的 Codex 完成态，忽略异常退出码并继续后续闸门。详情见: $($codexResult.StdoutLog)"
  }

  if ((($codexResult.ExitCode -ne 0) -and (-not $codexRecoverableCompletion)) -or $codexBlocked) {
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "Codex 会话未完成。" -TestSummary "未运行测试。" -Notes "Codex 输出包含失败或 BLOCKED，请人工查看。" -Stage1Summary "NOT_RUN - 未进入 Stage 1 审查。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = $executionPolicy.task_kind
      gate_profile = $executionPolicy.gate_profile
      required_truth_sources = $executionPolicy.required_truth_sources
      truth_source_state = $initialReviewContext.truth_source_state
      agent = "codex"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = "blocked"
      test_command = $task.test_command
      failed_stage = $null
      stage1_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 1 审查。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 2 审查。"
      }
      session_policy = $executionPolicy.session
      subagent_policy = $executionPolicy.subagents
      context_files = $executionPolicy.context_files
      run_profile_path = $executionPolicy.run_profile_path
      review_context = $initialReviewContext
      commands = @(@{ cmd = "codex exec"; exit_code = $codexResult.ExitCode })
      blocked_reason = "Codex 会话未完成。"
      final_message = $codexResult.Output
      logs = @{
        stdout = $codexResult.StdoutLog
        stderr = $codexResult.StderrLog
        prompt = $codexResult.PromptLog
        last_message = $codexResult.LastMessageLog
      }
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (Get-TraceLogFiles -CodexResults @($codexResult)) | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "BLOCKED: Codex 会话未完成。完整日志: $($codexResult.StdoutLog)"
    Show-LogTail -Path $codexResult.StderrLog
    Show-LogTail -Path $codexResult.StdoutLog
    return [PSCustomObject]@{
      Status = "blocked"
      TaskId = $task.id
      ExitCode = 2
    }
  }

  $reviewContext = Get-ReviewContextPaths -Root $ProjectRoot -ExecutionPolicy $executionPolicy

  Write-Step "实现完成，开始 Stage 1 审查。"
  $stage1Prompt = New-Stage1ReviewPrompt -Task $task -Root $ProjectRoot -ExecutionPolicy $executionPolicy -ReviewContext $reviewContext -ImplementationOutput $codexResult.Output
  $stage1Result = Invoke-CodexTask -Prompt $stage1Prompt -Root $ProjectRoot -Command $CodexCommand -LogDirectory $taskLogDirectory -TaskId "$($task.id)-stage1" -Sandbox $executionPolicy.codex.review_sandbox -ActivityLabel "Stage 1 审查" -DisableHooks $executionPolicy.codex.disable_hooks -CaptureJsonEvents:$CaptureJsonEvents
  $stage1Verdict = Get-ReviewVerdict -Output $stage1Result.ReviewOutput
  $stage1RecoverableCompletion = Test-RecoverableCodexCompletion -ExitCode $stage1Result.ExitCode -Output $stage1Result.Output -LastMessage $stage1Result.LastMessage

  if ($stage1RecoverableCompletion -and $stage1Result.ExitCode -ne 0) {
    Write-Step "Stage 1 审查存在可继续推进的异常退出码，按 verdict 继续裁决。"
  }

  if ((($stage1Result.ExitCode -ne 0) -and (-not $stage1RecoverableCompletion)) -or $stage1Verdict -ne "PASS") {
    $stage1TerminalStatus = Get-ReviewTerminalStatus -Verdict $stage1Verdict
    $stage1Note = Get-ReviewFailureNote -StageName "Stage 1" -Verdict $stage1Verdict
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "Codex 已完成实现，但 Stage 1 审查未通过。" -TestSummary "未运行测试。" -Notes $stage1Note -Stage1Summary "$stage1Verdict - 审查未通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = $executionPolicy.task_kind
      gate_profile = $executionPolicy.gate_profile
      required_truth_sources = $executionPolicy.required_truth_sources
      truth_source_state = $reviewContext.truth_source_state
      agent = "codex"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = "failed"
      test_command = $task.test_command
      failed_stage = "stage1_review"
      stage1_review = [PSCustomObject]@{
        verdict = $stage1Verdict
        exit_code = $stage1Result.ExitCode
        summary = "Stage 1 审查未通过。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 2 审查。"
      }
      session_policy = $executionPolicy.session
      subagent_policy = $executionPolicy.subagents
      context_files = $executionPolicy.context_files
      run_profile_path = $executionPolicy.run_profile_path
      review_context = $reviewContext
      commands = @(
        @{ cmd = "codex exec"; exit_code = $codexResult.ExitCode },
        @{ cmd = "stage1 review"; exit_code = $stage1Result.ExitCode; verdict = $stage1Verdict }
      )
      repair_queue = @("Stage 1 findings require repair or human unblock before retest.")
      final_message = $stage1Result.Output
      logs = @{
        implementation_stdout = $codexResult.StdoutLog
        implementation_stderr = $codexResult.StderrLog
        implementation_prompt = $codexResult.PromptLog
        implementation_last_message = $codexResult.LastMessageLog
        stage1_stdout = $stage1Result.StdoutLog
        stage1_stderr = $stage1Result.StderrLog
        stage1_prompt = $stage1Result.PromptLog
        stage1_last_message = $stage1Result.LastMessageLog
      }
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (Get-TraceLogFiles -CodexResults @($codexResult, $stage1Result)) | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "$($stage1TerminalStatus.ToUpperInvariant()): Stage 1 审查未通过。Verdict = $stage1Verdict"
    Show-LogTail -Path $stage1Result.StdoutLog
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = 1
    }
  }

  Write-Step "Stage 1 审查通过，开始运行测试。"
  $testResult = Invoke-TestCommand -Command $task.test_command -Root $ProjectRoot -LogDirectory $taskLogDirectory -TaskId $task.id

  if ($testResult.ExitCode -ne 0) {
    Write-Step "测试失败，开始自动失败归因。"
    $triageResult = Invoke-FailureTriage -Task $task -Root $ProjectRoot -ExecutionPolicy $executionPolicy -ReviewContext $reviewContext -CodexResult $codexResult -Stage1Result $stage1Result -TestResult $testResult -CodexCommand $CodexCommand -LogDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents
    $triageNote = if ($null -ne $triageResult.ReportPath) { "failure triage: $($triageResult.ReportPath)" } elseif (-not [string]::IsNullOrWhiteSpace($triageResult.ParseError)) { "failure triage parse failed: $($triageResult.ParseError)" } else { "failure triage did not produce a structured report" }
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "Codex 已产生改动。" -TestSummary "测试失败，退出码 $($testResult.ExitCode)。" -Notes "禁止标记 passes=true，请人工查看 trace；$triageNote" -Stage1Summary "PASS - 审查通过。" -Stage2Summary "NOT_RUN - 未进入 Stage 2 审查。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = $executionPolicy.task_kind
      gate_profile = $executionPolicy.gate_profile
      required_truth_sources = $executionPolicy.required_truth_sources
      truth_source_state = $reviewContext.truth_source_state
      agent = "codex"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = "failed"
      test_command = $task.test_command
      failed_stage = "test_command"
      stage1_review = [PSCustomObject]@{
        verdict = "PASS"
        exit_code = $stage1Result.ExitCode
        summary = "Stage 1 审查通过。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = "NOT_RUN"
        exit_code = $null
        summary = "未进入 Stage 2 审查。"
      }
      session_policy = $executionPolicy.session
      subagent_policy = $executionPolicy.subagents
      context_files = $executionPolicy.context_files
      run_profile_path = $executionPolicy.run_profile_path
      review_context = $reviewContext
      commands = @(
        @{ cmd = "codex exec"; exit_code = $codexResult.ExitCode },
        @{ cmd = "stage1 review"; exit_code = $stage1Result.ExitCode; verdict = $stage1Verdict },
        @{ cmd = $task.test_command; exit_code = $testResult.ExitCode },
        @{ cmd = "failure triage"; exit_code = $triageResult.ExitCode; report = $triageResult.ReportPath }
      )
      final_message = $codexResult.Output
      logs = @{
        implementation_stdout = $codexResult.StdoutLog
        implementation_stderr = $codexResult.StderrLog
        implementation_prompt = $codexResult.PromptLog
        implementation_last_message = $codexResult.LastMessageLog
        stage1_stdout = $stage1Result.StdoutLog
        stage1_stderr = $stage1Result.StderrLog
        stage1_prompt = $stage1Result.PromptLog
        stage1_last_message = $stage1Result.LastMessageLog
        test_output = $testResult.OutputLog
        failure_triage_stdout = $triageResult.StdoutLog
        failure_triage_stderr = $triageResult.StderrLog
        failure_triage_prompt = $triageResult.PromptLog
        failure_triage_last_message = $triageResult.LastMessageLog
      }
      failure_triage_report = $triageResult.ReportPath
      failure_triage_parse_error = $triageResult.ParseError
      failure_triage_summary = $triageNote
      test_result_summary = [PSCustomObject]@{
        command = $task.test_command
        exit_code = $testResult.ExitCode
        verdict = "FAIL"
        evidence = @(ConvertTo-StringArray -Value $testResult.OutputLog)
      }
      evidence_gate_status = "NOT_RUN"
      repair_queue = @("failure triage generated; inspect report and repair findings before rerun.")
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (@(Get-TraceLogFiles -CodexResults @($codexResult, $stage1Result, $triageResult)) + @(ConvertTo-StringArray -Value $testResult.OutputLog) + @(ConvertTo-StringArray -Value $triageResult.ReportPath)) | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "FAILED: 测试失败，退出码 $($testResult.ExitCode)。"
    Write-Output ""
    Write-Output "测试输出:"
    Write-Output $testResult.Output
    Write-Output ""
    Write-Output "Failure triage:"
    Write-Output $triageNote
    Write-Output ""
    Write-Output "Codex 完整日志: $($codexResult.StdoutLog)"
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = 1
    }
  }

  Write-Step "测试通过，开始 Stage 2 审查。"
  $changedFilesForStage2 = @(Get-GitChangedPaths -Root $ProjectRoot)
  $stage2Prompt = New-Stage2ReviewPrompt -Task $task -Root $ProjectRoot -ExecutionPolicy $executionPolicy -ReviewContext $reviewContext -TestResult $testResult -ImplementationOutput $codexResult.LastMessage -ChangedFiles $changedFilesForStage2
  $stage2Result = Invoke-CodexTask -Prompt $stage2Prompt -Root $ProjectRoot -Command $CodexCommand -LogDirectory $taskLogDirectory -TaskId "$($task.id)-stage2" -Sandbox $executionPolicy.codex.review_sandbox -ActivityLabel "Stage 2 审查" -DisableHooks $executionPolicy.codex.disable_hooks -CaptureJsonEvents:$CaptureJsonEvents
  $stage2Verdict = Get-ReviewVerdict -Output $stage2Result.ReviewOutput
  $stage2RecoverableCompletion = Test-RecoverableCodexCompletion -ExitCode $stage2Result.ExitCode -Output $stage2Result.Output -LastMessage $stage2Result.LastMessage

  if ($stage2RecoverableCompletion -and $stage2Result.ExitCode -ne 0) {
    Write-Step "Stage 2 审查存在可继续推进的异常退出码，按 verdict 继续裁决。"
  }

  if ((($stage2Result.ExitCode -ne 0) -and (-not $stage2RecoverableCompletion)) -or $stage2Verdict -ne "PASS") {
    $stage2TerminalStatus = Get-ReviewTerminalStatus -Verdict $stage2Verdict
    $stage2Note = Get-ReviewFailureNote -StageName "Stage 2" -Verdict $stage2Verdict
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "Codex 已完成实现并通过测试，但 Stage 2 审查未通过。" -TestSummary "测试命令通过。" -Notes $stage2Note -Stage1Summary "PASS - 审查通过。" -Stage2Summary "$stage2Verdict - 审查未通过。"
    $tracePayload = [PSCustomObject]@{
      task_id = $task.id
      task_session_id = $taskSessionId
      task_kind = $executionPolicy.task_kind
      gate_profile = $executionPolicy.gate_profile
      required_truth_sources = $executionPolicy.required_truth_sources
      truth_source_state = $reviewContext.truth_source_state
      agent = "codex"
      started_at = $startedAt.ToString("o")
      ended_at = (Get-Date).ToString("o")
      status = $stage2TerminalStatus
      test_command = $task.test_command
      failed_stage = "stage2_review"
      stage1_review = [PSCustomObject]@{
        verdict = "PASS"
        exit_code = $stage1Result.ExitCode
        summary = "Stage 1 审查通过。"
      }
      stage2_review = [PSCustomObject]@{
        verdict = $stage2Verdict
        exit_code = $stage2Result.ExitCode
        summary = "Stage 2 审查未通过。"
      }
      session_policy = $executionPolicy.session
      subagent_policy = $executionPolicy.subagents
      context_files = $executionPolicy.context_files
      run_profile_path = $executionPolicy.run_profile_path
      review_context = $reviewContext
      commands = @(
        @{ cmd = "codex exec"; exit_code = $codexResult.ExitCode },
        @{ cmd = "stage1 review"; exit_code = $stage1Result.ExitCode; verdict = $stage1Verdict },
        @{ cmd = $task.test_command; exit_code = $testResult.ExitCode },
        @{ cmd = "stage2 review"; exit_code = $stage2Result.ExitCode; verdict = $stage2Verdict }
      )
      evidence_gate_status = "NOT_RUN"
      repair_queue = @("Stage 2 findings require repair or human unblock before completion.")
      final_message = $stage2Result.Output
      logs = @{
        implementation_stdout = $codexResult.StdoutLog
        implementation_stderr = $codexResult.StderrLog
        implementation_prompt = $codexResult.PromptLog
        implementation_last_message = $codexResult.LastMessageLog
        stage1_stdout = $stage1Result.StdoutLog
        stage1_stderr = $stage1Result.StderrLog
        stage1_prompt = $stage1Result.PromptLog
        stage1_last_message = $stage1Result.LastMessageLog
        stage2_stdout = $stage2Result.StdoutLog
        stage2_stderr = $stage2Result.StderrLog
        stage2_prompt = $stage2Result.PromptLog
        stage2_last_message = $stage2Result.LastMessageLog
        test_output = $testResult.OutputLog
      }
      test_result_summary = [PSCustomObject]@{
        command = $task.test_command
        exit_code = $testResult.ExitCode
        verdict = "PASS"
        evidence = @(ConvertTo-StringArray -Value $testResult.OutputLog)
      }
    }
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (@(Get-TraceLogFiles -CodexResults @($codexResult, $stage1Result, $stage2Result)) + @(ConvertTo-StringArray -Value $testResult.OutputLog)) | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "$($stage2TerminalStatus.ToUpperInvariant()): Stage 2 审查未通过。Verdict = $stage2Verdict"
    Show-LogTail -Path $stage2Result.StdoutLog
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = 1
    }
  }

  $changedFiles = & git -C $ProjectRoot status --short
  $tracePayload = [PSCustomObject]@{
    task_id = $task.id
    task_session_id = $taskSessionId
    task_kind = $executionPolicy.task_kind
    gate_profile = $executionPolicy.gate_profile
    required_truth_sources = $executionPolicy.required_truth_sources
    truth_source_state = $reviewContext.truth_source_state
    agent = "codex"
    started_at = $startedAt.ToString("o")
    ended_at = (Get-Date).ToString("o")
    status = "passed"
    test_command = $task.test_command
    failed_stage = $null
    stage1_review = [PSCustomObject]@{
      verdict = "PASS"
      exit_code = $stage1Result.ExitCode
      summary = "Stage 1 审查通过。"
    }
    stage2_review = [PSCustomObject]@{
      verdict = "PASS"
      exit_code = $stage2Result.ExitCode
      summary = "Stage 2 审查通过。"
    }
    session_policy = $executionPolicy.session
    subagent_policy = $executionPolicy.subagents
    context_files = $executionPolicy.context_files
    run_profile_path = $executionPolicy.run_profile_path
    review_context = $reviewContext
    commands = @(
      @{ cmd = "codex exec"; exit_code = $codexResult.ExitCode },
      @{ cmd = "stage1 review"; exit_code = $stage1Result.ExitCode; verdict = $stage1Verdict },
      @{ cmd = $task.test_command; exit_code = $testResult.ExitCode },
      @{ cmd = "stage2 review"; exit_code = $stage2Result.ExitCode; verdict = $stage2Verdict }
    )
    files_changed = $changedFiles
    final_message = $stage2Result.Output
    logs = @{
      implementation_stdout = $codexResult.StdoutLog
      implementation_stderr = $codexResult.StderrLog
      implementation_prompt = $codexResult.PromptLog
      implementation_last_message = $codexResult.LastMessageLog
      stage1_stdout = $stage1Result.StdoutLog
      stage1_stderr = $stage1Result.StderrLog
      stage1_prompt = $stage1Result.PromptLog
      stage1_last_message = $stage1Result.LastMessageLog
      stage2_stdout = $stage2Result.StdoutLog
      stage2_stderr = $stage2Result.StderrLog
      stage2_prompt = $stage2Result.PromptLog
      stage2_last_message = $stage2Result.LastMessageLog
      test_output = $testResult.OutputLog
    }
    test_result_summary = [PSCustomObject]@{
      command = $task.test_command
      exit_code = $testResult.ExitCode
      verdict = "PASS"
      evidence = @(ConvertTo-StringArray -Value $testResult.OutputLog)
    }
    evidence_gate_status = "UNKNOWN"
  }
  $evidenceGateFindings = @(Get-EvidenceGateFindings -Task $task -ImplementationOutput $codexResult.LastMessage)
  if ($evidenceGateFindings.Count -gt 0) {
    $tracePayload.failed_stage = "evidence_gate"
    $tracePayload.status = "failed"
    $tracePayload.evidence_gate_status = "FAIL"
    $tracePayload.repair_queue = @(ConvertTo-RepairQueueFromFindings -Findings $evidenceGateFindings)
    $tracePayload.blocked_reason = "implementation summary missing required evidence sections"
    $tracePayload.ended_at = (Get-Date).ToString("o")
    Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (@(Get-TraceLogFiles -CodexResults @($codexResult, $stage1Result, $stage2Result)) + @(ConvertTo-StringArray -Value $testResult.OutputLog)) | Out-Null
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "实现、测试和审查已完成，但 evidence gate 未通过。" -TestSummary "测试命令通过。" -Notes ($evidenceGateFindings -join "`n") -Stage1Summary "PASS - 审查通过。" -Stage2Summary "PASS - 审查通过。"
    Write-Step "FAILED: evidence gate 未通过。"
    Write-Output ($evidenceGateFindings -join "`n")
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = 1
    }
  }
  $tracePayload.evidence_gate_status = "PASS"
  Add-TraceDurableFields -Trace $tracePayload -Root $ProjectRoot -SessionDirectory $taskLogDirectory -CaptureJsonEvents:$CaptureJsonEvents -EventsSource $codexResult -LogFiles (@(Get-TraceLogFiles -CodexResults @($codexResult, $stage1Result, $stage2Result)) + @(ConvertTo-StringArray -Value $testResult.OutputLog)) | Out-Null
  $ownershipResult = Test-CommitPathOwnership -Root $ProjectRoot -Task $task -RuntimeAllowedPaths @(
    $TaskFile,
    $ProgressFile,
    "$TraceDir/",
    "traces/",
    "artifacts/"
  ) -NonBlockingDirtyPaths $nonBlockingDirtyPaths
  if (-not $ownershipResult.Passed) {
    $ownershipNotes = @()
    if (@($ownershipResult.UnexpectedPaths).Count -gt 0) {
      $ownershipNotes += ("unexpected paths: " + ($ownershipResult.UnexpectedPaths -join ", "))
    }
    if (@($ownershipResult.ForbiddenPaths).Count -gt 0) {
      $ownershipNotes += ("forbidden paths: " + ($ownershipResult.ForbiddenPaths -join ", "))
    }
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "实现、测试和审查已完成，但 commit ownership gate 未通过。" -TestSummary "测试命令通过。" -Notes ($ownershipNotes -join "`n") -Stage1Summary "PASS - 审查通过。" -Stage2Summary "PASS - 审查通过。"
    Add-Member -InputObject $tracePayload -MemberType NoteProperty -Name "unexpected_paths" -Value $ownershipResult.UnexpectedPaths -Force
    $tracePayload.ownership_check = [PSCustomObject]@{
      status = $stage1TerminalStatus
      unexpected_paths = $ownershipResult.UnexpectedPaths
      forbidden_paths = $ownershipResult.ForbiddenPaths
    }
    $tracePayload.boundary_violations = @($ownershipResult.UnexpectedPaths + $ownershipResult.ForbiddenPaths)
    $tracePayload.failed_stage = "commit_path_ownership"
    $tracePayload.status = "failed"
    $tracePayload.blocked_reason = "changed paths 超出 owned_paths / runtime allowlist 或命中 forbidden_paths"
    $tracePayload.files_changed = & git -C $ProjectRoot status --short
    $tracePayload.ended_at = (Get-Date).ToString("o")
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "FAILED: commit ownership gate 未通过。"
    if (@($ownershipResult.UnexpectedPaths).Count -gt 0) {
      Write-Output ("Unexpected paths: " + ($ownershipResult.UnexpectedPaths -join ", "))
    }
    if (@($ownershipResult.ForbiddenPaths).Count -gt 0) {
      Write-Output ("Forbidden paths: " + ($ownershipResult.ForbiddenPaths -join ", "))
    }
    return [PSCustomObject]@{
      Status = $stage1TerminalStatus
      TaskId = $task.id
      ExitCode = $(if ($stage1TerminalStatus -eq "blocked") { 2 } else { 1 })
    }
  }

  $tracePayload.ownership_check = [PSCustomObject]@{
    status = "passed"
    unexpected_paths = @()
    forbidden_paths = @()
  }

  $dependencyChangeResult = Test-DependencyChangeApproval -Task $task -ChangedPaths $tracePayload.files_changed
  $tracePayload.dependency_changes = [PSCustomObject]@{
    status = $(if ($dependencyChangeResult.Passed) { "approved_or_not_changed" } else { "failed" })
    changed_files = $dependencyChangeResult.ChangedFiles
    allowed_dependencies = $dependencyChangeResult.AllowedDependencies
  }
  if (-not $dependencyChangeResult.Passed) {
    Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "实现、测试和审查已完成，但 dependency gate 未通过。" -TestSummary "测试命令通过。" -Notes ("dependency manifests changed without approved allowed_dependencies: " + ($dependencyChangeResult.ChangedFiles -join ", ")) -Stage1Summary "PASS - 审查通过。" -Stage2Summary "PASS - 审查通过。"
    $tracePayload.failed_stage = "dependency_change_gate"
    $tracePayload.status = "failed"
    $tracePayload.blocked_reason = "dependency manifest changed without approved allowed_dependencies"
    $tracePayload.boundary_violations = @($tracePayload.boundary_violations + $dependencyChangeResult.ChangedFiles)
    $tracePayload.files_changed = & git -C $ProjectRoot status --short
    $tracePayload.ended_at = (Get-Date).ToString("o")
    Save-Trace -Directory $TracePath -Trace $tracePayload | Out-Null
    Write-Step "FAILED: dependency gate 未通过。"
    Write-Output ("Dependency manifests changed: " + ($dependencyChangeResult.ChangedFiles -join ", "))
    return [PSCustomObject]@{
      Status = $stage2TerminalStatus
      TaskId = $task.id
      ExitCode = $(if ($stage2TerminalStatus -eq "blocked") { 2 } else { 1 })
    }
  }

  Set-TaskPassed -TaskDocument $taskDocument -TaskId $task.id -Path $TaskPath
  Write-ProgressEntry -Path $ProgressPath -Task $task -WorkSummary "完成当前任务并更新 task.json。" -TestSummary "Stage 1、测试命令、Stage 2 全部通过。" -Notes "可继续运行 driver 处理下一个任务。" -Stage1Summary "PASS - 审查通过。" -Stage2Summary "PASS - 审查通过。"
  $tracePayload.files_changed = & git -C $ProjectRoot status --short
  $tracePayload.ended_at = (Get-Date).ToString("o")
  $traceFile = Save-Trace -Directory $TracePath -Trace $tracePayload

  $gitAddResult = Invoke-NativeCommandQuiet -Script { & git -C $ProjectRoot add --all }
  if ($gitAddResult.ExitCode -ne 0) {
    Write-Step "FAILED: git add 失败。"
    Write-Output $gitAddResult.Output
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = $gitAddResult.ExitCode
    }
  }

  $gitCommitResult = Invoke-NativeCommandQuiet -Script { & git -C $ProjectRoot commit -m "feat: $($task.description) - completed $($task.id)" }
  if ($gitCommitResult.ExitCode -ne 0) {
    Write-Step "FAILED: git commit 失败。"
    Write-Output $gitCommitResult.Output
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = $gitCommitResult.ExitCode
    }
  }

  $gitHashResult = Invoke-NativeCommandQuiet -Script { & git -C $ProjectRoot rev-parse --short HEAD }
  if ($gitHashResult.ExitCode -ne 0) {
    Write-Step "FAILED: 无法读取 commit hash。"
    Write-Output $gitHashResult.Output
    return [PSCustomObject]@{
      Status = "failed"
      TaskId = $task.id
      ExitCode = $gitHashResult.ExitCode
    }
  }

  $commitHash = $gitHashResult.Output.Trim()
  Write-Step "任务完成: $($task.id)"
  Write-Output "Commit: $commitHash"
  Write-Output "Trace: $traceFile"
  Write-Output "Codex 日志: $($codexResult.StdoutLog)"

  return [PSCustomObject]@{
    Status = "passed"
    TaskId = $task.id
    ExitCode = 0
  }
}

$taskPath = Join-Path $ProjectRoot $TaskFile
$progressPath = Join-Path $ProjectRoot $ProgressFile
$tracePath = Join-Path $ProjectRoot $TraceDir

Write-Step "运行 runtime doctor。"
$doctorResult = Invoke-RuntimeDoctor -Root $ProjectRoot
if ($doctorResult.ExitCode -ne 0) {
  Write-Step "BLOCKED: runtime doctor 未通过。"
  Write-Output $doctorResult.Output
  exit 2
}
Write-Step "Runtime doctor 通过。"

do {
  $iteration = @(Invoke-OneTask -TaskPath $taskPath -ProgressPath $progressPath -TracePath $tracePath) | Select-Object -Last 1

  if ($iteration.Status -eq "idle") {
    Write-Output "全部任务已完成或没有依赖满足的任务。"
    exit 0
  }

  if ($iteration.Status -ne "passed") {
    exit $iteration.ExitCode
  }
}
while ($RunUntilDone)
