param(
  [string]$ProjectRoot = (Get-Location).Path,
  [switch]$WriteExperience
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$managedAgentsStartMarker = "<!-- BEGIN AUTO-MODULE-FACTS -->"
$managedAgentsEndMarker = "<!-- END AUTO-MODULE-FACTS -->"
$moduleAgentsTargetNames = @("src", "app", "apps", "packages", "services", "modules", "features", "frontend", "backend", "tests", "test")
$moduleAgentsIgnoredPrefixes = @(
  ".git/",
  ".codex/",
  ".codex-harness/",
  ".agents/",
  "docs/",
  "traces/",
  "artifacts/",
  "dist/",
  "build/",
  "coverage/",
  "node_modules/"
)
$moduleApiFilePatterns = @(
  '(^|/)(index|main|app|mod|entry)\.',
  '/(api|services|controllers|routes|router|hooks|store|models|components|pages|views)/'
)

function Get-ObjectPropertyValue {
  param(
    [object]$InputObject,
    [string]$Name,
    [object]$Default = $null
  )

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

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

  return $property.Value
}

function Add-Finding {
  param(
    [System.Collections.Generic.List[object]]$Findings,
    [string]$Severity,
    [string]$Category,
    [string]$Message,
    [string]$RepairMode,
    [string]$TargetDoc = "docs/harness/governance-auto-repair.md"
  )

  $Findings.Add([pscustomobject]@{
    severity = $Severity
    category = $Category
    message = $Message
    repair_mode = $RepairMode
    target_doc = $TargetDoc
  })
}

function Get-RelativePathCompat {
  param(
    [string]$Root,
    [string]$Path
  )

  $rootFull = ([System.IO.Path]::GetFullPath($Root)).TrimEnd('\', '/') + '\'
  $pathFull = [System.IO.Path]::GetFullPath($Path)
  if ($pathFull.StartsWith($rootFull, [System.StringComparison]::OrdinalIgnoreCase)) {
    return $pathFull.Substring($rootFull.Length).Replace('\', '/')
  }

  return $pathFull.Replace('\', '/')
}

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

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

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

    return @($Value)
  }

  $items = @()
  foreach ($item in $Value) {
    if ($null -ne $item) {
      $text = [string]$item
      if (-not [string]::IsNullOrWhiteSpace($text)) {
        $items += $text
      }
    }
  }

  return $items
}

function Get-GovernancePolicy {
  param([string]$Root)

  $policy = [ordered]@{
    decision_log = "docs/ai/AUTO_DECISIONS.md"
    debt_log = "docs/harness/governance-auto-repair.md"
    failed_trace_mode = "auto_repair"
    ambiguity_mode = "auto_resolve"
    env_fallback = "docker"
  }

  $taskDocument = Read-TaskDocument -Root $Root
  if ($null -eq $taskDocument) {
    return [pscustomobject]$policy
  }

  $runtime = Get-ObjectPropertyValue -InputObject $taskDocument -Name "runtime"
  $governance = Get-ObjectPropertyValue -InputObject $runtime -Name "governance"
  if ($null -eq $governance) {
    return [pscustomobject]$policy
  }

  foreach ($key in @("decision_log", "debt_log", "failed_trace_mode", "ambiguity_mode", "env_fallback")) {
    $value = [string](Get-ObjectPropertyValue -InputObject $governance -Name $key -Default "")
    if (-not [string]::IsNullOrWhiteSpace($value)) {
      $policy[$key] = $value
    }
  }

  return [pscustomobject]$policy
}

function ConvertTo-BulletLines {
  param(
    [string[]]$Items,
    [string]$EmptyLine = "- (none)"
  )

  $normalized = @($Items | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
  if ($normalized.Count -eq 0) {
    return $EmptyLine
  }

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

function Get-TrackedOrModifiedFiles {
  param([string]$Root)

  $statusLines = & git -C $Root status --short --untracked-files=all 2>$null
  if ($LASTEXITCODE -ne 0) {
    return @()
  }

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

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

    if ($relativePath.Contains(" -> ")) {
      $relativePath = ($relativePath -split " -> ")[-1].Trim()
    }

    $paths += $relativePath.Replace('\', '/')
  }

  return @($paths | Select-Object -Unique)
}

function Read-TaskDocument {
  param([string]$Root)

  $taskPath = Join-Path $Root "task.json"
  if (-not (Test-Path -LiteralPath $taskPath)) {
    return $null
  }

  try {
    return (Get-Content -LiteralPath $taskPath -Raw | ConvertFrom-Json)
  }
  catch {
    return $null
  }
}

function Test-IgnoredModulePath {
  param([string]$Path)

  foreach ($prefix in $moduleAgentsIgnoredPrefixes) {
    if ($Path.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
      return $true
    }
  }

  return $false
}

function Resolve-NearestManagedAgentsDirectory {
  param(
    [string]$Root,
    [string]$RelativePath
  )

  $segments = @($RelativePath.Replace('\', '/').Split('/', [System.StringSplitOptions]::RemoveEmptyEntries))
  if ($segments.Count -lt 2) {
    return $null
  }

  $candidateCount = $segments.Count - 1
  for ($length = $candidateCount; $length -ge 1; $length--) {
    $candidateSegments = $segments[0..($length - 1)]
    $candidateRelative = ($candidateSegments -join '/')
    $leafName = $candidateSegments[-1]
    if ($leafName -notin $moduleAgentsTargetNames) {
      continue
    }

    $candidateFull = Join-Path $Root $candidateRelative
    if (Test-Path -LiteralPath $candidateFull -PathType Container) {
      return [pscustomobject]@{
        RelativePath = $candidateRelative
        FullPath = $candidateFull
      }
    }
  }

  return $null
}

function Get-OwnedPathModuleCandidates {
  param([string]$Root)

  $taskDocument = Read-TaskDocument -Root $Root
  if ($null -eq $taskDocument) {
    return @()
  }

  $candidates = @()
  $tasks = @(Get-ObjectPropertyValue -InputObject $taskDocument -Name "tasks" -Default @())
  foreach ($task in $tasks) {
    $ownedPaths = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $task -Name "owned_paths" -Default @()))
    foreach ($ownedPath in $ownedPaths) {
      $normalizedOwnedPath = $ownedPath.Replace('\', '/').Trim('/')
      if ([string]::IsNullOrWhiteSpace($normalizedOwnedPath)) {
        continue
      }

      if (Test-IgnoredModulePath -Path ($normalizedOwnedPath + '/')) {
        continue
      }

      $fullPath = Join-Path $Root $normalizedOwnedPath
      if (Test-Path -LiteralPath $fullPath -PathType Container) {
        $candidates += [pscustomobject]@{
          RelativePath = $normalizedOwnedPath
          FullPath = $fullPath
          Source = "owned_paths"
        }
      }
    }
  }

  return @($candidates | Sort-Object RelativePath -Unique)
}

function Get-RepoMapModuleCandidates {
  param([string]$Root)

  $repoMapPath = Join-Path $Root "docs\ai\repo-map.md"
  if (-not (Test-Path -LiteralPath $repoMapPath)) {
    return @()
  }

  $content = Get-Content -LiteralPath $repoMapPath -Raw
  $matches = [System.Text.RegularExpressions.Regex]::Matches($content, '`([^`]+)`')
  $candidates = @()
  foreach ($match in $matches) {
    $candidate = $match.Groups[1].Value.Replace('\', '/').Trim('/')
    if ([string]::IsNullOrWhiteSpace($candidate)) {
      continue
    }

    if (
      $candidate -match '[*?"<>|]' -or
      $candidate.Contains(': ') -or
      $candidate.Contains(' -> ') -or
      $candidate.Contains(',') -or
      $candidate.Contains('`') -or
      $candidate.Contains('$') -or
      $candidate.Contains('(') -or
      $candidate.Contains(')') -or
      $candidate.Contains('\') -or
      $candidate -notmatch '^[A-Za-z0-9._/\-]+$'
    ) {
      continue
    }

    if (Test-IgnoredModulePath -Path ($candidate + '/')) {
      continue
    }

    $fullPath = Join-Path $Root $candidate
    if (Test-Path -LiteralPath $fullPath -PathType Container) {
      $candidates += [pscustomobject]@{
        RelativePath = $candidate
        FullPath = $fullPath
        Source = "repo_map"
      }
    }
  }

  return @($candidates | Sort-Object RelativePath -Unique)
}

function Resolve-NearestModuleCandidate {
  param(
    [string]$RelativePath,
    [object[]]$Candidates
  )

  $normalizedPath = $RelativePath.Replace('\', '/')
  $best = $null
  foreach ($candidate in @($Candidates)) {
    $candidatePath = [string]$candidate.RelativePath
    if ([string]::IsNullOrWhiteSpace($candidatePath)) {
      continue
    }

    if (
      $normalizedPath.Equals($candidatePath, [System.StringComparison]::OrdinalIgnoreCase) -or
      $normalizedPath.StartsWith($candidatePath + '/', [System.StringComparison]::OrdinalIgnoreCase)
    ) {
      if ($null -eq $best -or $candidatePath.Length -gt ([string]$best.RelativePath).Length) {
        $best = $candidate
      }
    }
  }

  return $best
}

function Get-ModuleAgentsTargets {
  param([string]$Root)

  $candidateDirectories = @()
  $candidateDirectories += Get-OwnedPathModuleCandidates -Root $Root
  $candidateDirectories += Get-RepoMapModuleCandidates -Root $Root

  $changedPaths = Get-TrackedOrModifiedFiles -Root $Root
  $targets = @()
  foreach ($path in $changedPaths) {
    if ([string]::IsNullOrWhiteSpace($path)) {
      continue
    }

    if ($path.EndsWith("/AGENTS.md", [System.StringComparison]::OrdinalIgnoreCase) -or $path -eq "AGENTS.md") {
      continue
    }

    if (Test-IgnoredModulePath -Path $path) {
      continue
    }

    $target = Resolve-NearestModuleCandidate -RelativePath $path -Candidates $candidateDirectories
    if ($null -eq $target) {
      $target = Resolve-NearestManagedAgentsDirectory -Root $Root -RelativePath $path
    }

    if ($null -ne $target) {
      $targets += $target
    }
  }

  return @($targets | Sort-Object RelativePath -Unique)
}

function Get-ModuleFilesForDirectory {
  param(
    [string]$Root,
    [string]$ModuleRelativePath
  )

  $prefix = $ModuleRelativePath.TrimEnd('/') + '/'
  return @(
    Get-TrackedOrModifiedFiles -Root $Root |
      Where-Object {
        $_.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) -and
        -not $_.EndsWith("/AGENTS.md", [System.StringComparison]::OrdinalIgnoreCase)
      } |
      Select-Object -Unique
  )
}

function Get-ValidationCommandForModule {
  param(
    [string]$Root,
    [string]$ModuleRelativePath
  )

  $taskPath = Join-Path $Root "task.json"
  if (-not (Test-Path -LiteralPath $taskPath)) {
    return "git diff --check"
  }

  try {
    $taskDocument = Get-Content -LiteralPath $taskPath -Raw | ConvertFrom-Json
  }
  catch {
    return "git diff --check"
  }

  $tasks = @(Get-ObjectPropertyValue -InputObject $taskDocument -Name "tasks" -Default @())
  foreach ($task in $tasks) {
    $ownedPaths = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $task -Name "owned_paths" -Default @()))
    foreach ($ownedPath in $ownedPaths) {
      $normalizedOwnedPath = $ownedPath.Replace('\', '/').TrimEnd('/')
      if ([string]::IsNullOrWhiteSpace($normalizedOwnedPath)) {
        continue
      }

      if (
        $ModuleRelativePath.Equals($normalizedOwnedPath, [System.StringComparison]::OrdinalIgnoreCase) -or
        $ModuleRelativePath.StartsWith($normalizedOwnedPath + '/', [System.StringComparison]::OrdinalIgnoreCase) -or
        $normalizedOwnedPath.StartsWith($ModuleRelativePath + '/', [System.StringComparison]::OrdinalIgnoreCase)
      ) {
        $command = [string](Get-ObjectPropertyValue -InputObject $task -Name "test_command" -Default "")
        if (-not [string]::IsNullOrWhiteSpace($command)) {
          return $command.Trim()
        }

        $developmentValidation = Get-ObjectPropertyValue -InputObject $task -Name "development_validation"
        $candidate = [string](Get-ObjectPropertyValue -InputObject $developmentValidation -Name "command" -Default "")
        if (-not [string]::IsNullOrWhiteSpace($candidate)) {
          return $candidate.Trim()
        }

        $acceptanceValidation = Get-ObjectPropertyValue -InputObject $task -Name "acceptance_validation"
        $candidate = [string](Get-ObjectPropertyValue -InputObject $acceptanceValidation -Name "command" -Default "")
        if (-not [string]::IsNullOrWhiteSpace($candidate)) {
          return $candidate.Trim()
        }
      }
    }
  }

  return "git diff --check"
}

function Build-ManagedModuleFactsBlock {
  param(
    [string]$Root,
    [string]$ModuleRelativePath
  )

  $moduleFiles = Get-ModuleFilesForDirectory -Root $Root -ModuleRelativePath $ModuleRelativePath
  $entryPoints = @(
    $moduleFiles |
      Where-Object {
        $matched = $false
        foreach ($pattern in $moduleApiFilePatterns) {
          if ($_ -match $pattern) {
            $matched = $true
            break
          }
        }
        $matched
      } |
      Select-Object -First 8
  )
  $validationCommand = Get-ValidationCommandForModule -Root $Root -ModuleRelativePath $ModuleRelativePath

  return @"
$managedAgentsStartMarker
## Auto Module Facts

- Scope: `$ModuleRelativePath/`
- Last auto refresh: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz")
- Refresh source: changed paths under this directory

## Changed Files
$(ConvertTo-BulletLines -Items $moduleFiles)

## Likely Entry Points
$(ConvertTo-BulletLines -Items $entryPoints)

## Validation
- Preferred command: `$validationCommand`

## Maintenance Notes
- This block is maintained by `scripts/harness/harness-governance-check.ps1`.
- Keep hand-written rules outside this block.
$managedAgentsEndMarker
"@
}

function Update-ManagedModuleAgents {
  param(
    [string]$Root,
    [string]$ModuleRelativePath,
    [string]$ModuleFullPath
  )

  $agentsPath = Join-Path $ModuleFullPath "AGENTS.md"
  $managedBlock = Build-ManagedModuleFactsBlock -Root $Root -ModuleRelativePath $ModuleRelativePath

  function Get-NormalizedAgentsContent {
    param([string]$Content)

    return ($Content.TrimEnd() + "`r`n")
  }

  if (Test-Path -LiteralPath $agentsPath) {
    $existing = Get-Content -LiteralPath $agentsPath -Raw
    if ($existing.Contains($managedAgentsStartMarker) -and $existing.Contains($managedAgentsEndMarker)) {
      $updated = [System.Text.RegularExpressions.Regex]::Replace(
        $existing,
        "(?s)<!-- BEGIN AUTO-MODULE-FACTS -->.*?<!-- END AUTO-MODULE-FACTS -->",
        [System.Text.RegularExpressions.MatchEvaluator]{ param($match) $managedBlock.TrimEnd() }
      )
      $normalizedUpdated = Get-NormalizedAgentsContent -Content $updated
      if ($updated -ne $existing) {
        Set-Content -LiteralPath $agentsPath -Value $normalizedUpdated -Encoding UTF8 -NoNewline
        return "updated"
      }

      return "kept"
    }

    $updated = ($existing.TrimEnd() + "`r`n`r`n" + $managedBlock.Trim() + "`r`n")
    Set-Content -LiteralPath $agentsPath -Value (Get-NormalizedAgentsContent -Content $updated) -Encoding UTF8 -NoNewline
    return "appended"
  }

  $content = @"
# $ModuleRelativePath AGENTS.md

## 作用范围

- 本文件只适用于 `$ModuleRelativePath/` 下的改动。
- 根 `AGENTS.md` 仍然有效；本文件只补充本目录特有规则。

$managedBlock
"@
  Set-Content -LiteralPath $agentsPath -Value (Get-NormalizedAgentsContent -Content $content) -Encoding UTF8 -NoNewline
  return "created"
}

function Sync-ModuleAgents {
  param(
    [string]$Root,
    [System.Collections.Generic.List[object]]$Findings
  )

  $targets = Get-ModuleAgentsTargets -Root $Root
  $updates = @()
  foreach ($target in $targets) {
    $result = Update-ManagedModuleAgents -Root $Root -ModuleRelativePath $target.RelativePath -ModuleFullPath $target.FullPath
    if ($result -ne "kept") {
      $updates += [pscustomobject]@{
        module = $target.RelativePath
        result = $result
        agents = "$($target.RelativePath)/AGENTS.md"
      }
    }
  }

  foreach ($update in $updates) {
    Add-Finding `
      -Findings $Findings `
      -Severity "warn" `
      -Category "module_agents_auto_refresh" `
      -Message ("Auto-refreshed {0} ({1})." -f $update.agents, $update.result) `
      -RepairMode "deterministic_repair" `
      -TargetDoc $update.agents
  }

  return @($updates)
}

function Test-MergeMarkers {
  param(
    [string]$Root,
    [System.Collections.Generic.List[object]]$Findings
  )

  $scanRoots = @(
    "AGENTS.md",
    ".codex",
    "docs/harness",
    "docs/codex-harness-engineering"
  )

  foreach ($scanRoot in $scanRoots) {
    $fullPath = Join-Path $Root $scanRoot
    if (-not (Test-Path -LiteralPath $fullPath)) {
      continue
    }

    $files = if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
      @(Get-Item -LiteralPath $fullPath)
    }
    else {
      @(Get-ChildItem -LiteralPath $fullPath -Recurse -File -Include *.md,*.json,*.toml,*.ps1,*.rules)
    }

    foreach ($file in $files) {
      $content = Get-Content -LiteralPath $file.FullName -Raw -ErrorAction SilentlyContinue
      if ($content -match '(?m)^(<<<<<<<|=======|>>>>>>>)') {
        Add-Finding `
          -Findings $Findings `
          -Severity "block" `
          -Category "merge_marker" `
          -Message ("Merge marker found in {0}." -f (Get-RelativePathCompat -Root $Root -Path $file.FullName)) `
          -RepairMode "agent_repair" `
          -TargetDoc "docs/harness/rule-governance.md"
      }
    }
  }
}

function Test-HookSurface {
  param(
    [string]$Root,
    [System.Collections.Generic.List[object]]$Findings
  )

  $hookConfigPath = Join-Path $Root ".codex\hooks.json"
  if (-not (Test-Path -LiteralPath $hookConfigPath)) {
    return
  }

  $hookText = Get-Content -LiteralPath $hookConfigPath -Raw
  if ($hookText.Contains("hook-stop-verify.ps1")) {
    $activeScript = Join-Path $Root "tools\harness\hook-stop-verify.ps1"
    $templateScript = Join-Path $Root "docs\codex-harness-engineering\templates\hooks\hook-stop-verify.ps1"
    $installedTemplateScript = Join-Path $Root ".agents\docs\codex-harness-engineering\templates\hooks\hook-stop-verify.ps1"

    if (-not (Test-Path -LiteralPath $activeScript)) {
      $repairMode = if ((Test-Path -LiteralPath $templateScript) -or (Test-Path -LiteralPath $installedTemplateScript)) {
        "deterministic_repair"
      }
      else {
        "agent_repair"
      }

      Add-Finding `
        -Findings $Findings `
        -Severity "block" `
        -Category "missing_stop_hook_script" `
        -Message "hooks.json references tools/harness/hook-stop-verify.ps1, but the script is missing." `
        -RepairMode $repairMode `
        -TargetDoc "docs/harness/governance-auto-repair.md"
    }
  }
}

function Test-RulesSurface {
  param(
    [string]$Root,
    [System.Collections.Generic.List[object]]$Findings
  )

  $rulesDir = Join-Path $Root ".codex\rules"
  if (-not (Test-Path -LiteralPath $rulesDir)) {
    return
  }

  $rulesFiles = @(Get-ChildItem -LiteralPath $rulesDir -File -Filter "*.rules" -ErrorAction SilentlyContinue)
  if ($rulesFiles.Count -eq 0) {
    Add-Finding `
      -Findings $Findings `
      -Severity "warn" `
      -Category "missing_execpolicy_rules" `
      -Message ".codex/rules contains semantic Markdown rules but no official *.rules execution policy files." `
      -RepairMode "deterministic_repair" `
      -TargetDoc "docs/harness/rule-governance.md"
  }
}

function Test-ConfigSurface {
  param(
    [string]$Root,
    [System.Collections.Generic.List[object]]$Findings
  )

  $configPath = Join-Path $Root ".codex\config.toml"
  if (-not (Test-Path -LiteralPath $configPath)) {
    return
  }

  $configText = Get-Content -LiteralPath $configPath -Raw
  if ($configText -match "project_doc_fallback_filenames") {
    $hasAgents = Test-Path -LiteralPath (Join-Path $Root "AGENTS.md")
    $hasTeamGuide = Test-Path -LiteralPath (Join-Path $Root "TEAM_GUIDE.md")
    $hasDotAgents = Test-Path -LiteralPath (Join-Path $Root ".agents.md")
    if ($hasAgents -and (-not $hasTeamGuide) -and (-not $hasDotAgents)) {
      Add-Finding `
        -Findings $Findings `
        -Severity "warn" `
        -Category "stale_fallback_config" `
        -Message "project_doc_fallback_filenames is configured, but AGENTS.md exists and fallback files are missing." `
        -RepairMode "agent_repair" `
        -TargetDoc "docs/harness/rule-governance.md"
    }
  }
}

function Get-LatestTraceFile {
  param([string]$Root)

  $traceDir = Join-Path $Root "traces"
  if (-not (Test-Path -LiteralPath $traceDir)) {
    return $null
  }

  return @(Get-ChildItem -LiteralPath $traceDir -Recurse -File -Filter "*.json" -ErrorAction SilentlyContinue |
    Sort-Object LastWriteTimeUtc -Descending |
    Select-Object -First 1)[0]
}

function Get-GovernanceRepairFiles {
  param([string]$Root)

  $relativePaths = @(
    "AGENTS.md",
    "docs/harness",
    "tools/harness",
    "scripts/harness",
    "tools/install",
    ".codex/rules",
    "tools/harness/templates",
    "docs/codex-harness-engineering/templates"
  )

  $files = @()
  foreach ($relativePath in $relativePaths) {
    $fullPath = Join-Path $Root $relativePath
    if (-not (Test-Path -LiteralPath $fullPath)) {
      continue
    }

    if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
      $files += Get-Item -LiteralPath $fullPath
      continue
    }

    $files += Get-ChildItem -LiteralPath $fullPath -Recurse -File -ErrorAction SilentlyContinue
  }

  return @($files | Sort-Object FullName -Unique)
}

function Test-StaleTraceExperience {
  param(
    [string]$Root,
    [datetime]$TraceLastWriteTimeUtc
  )

  foreach ($file in @(Get-GovernanceRepairFiles -Root $Root)) {
    if ($file.LastWriteTimeUtc -gt $TraceLastWriteTimeUtc) {
      return $true
    }
  }

  return $false
}

function New-ExperienceFromTrace {
  param([string]$Root)

  $traceFile = Get-LatestTraceFile -Root $Root
  if ($null -eq $traceFile) {
    return $null
  }

  try {
    $trace = Get-Content -LiteralPath $traceFile.FullName -Raw | ConvertFrom-Json
  }
  catch {
    return $null
  }

  $status = [string](Get-ObjectPropertyValue -InputObject $trace -Name "status" -Default "")
  if ($status -notin @("blocked", "failed")) {
    return $null
  }

  if (Test-StaleTraceExperience -Root $Root -TraceLastWriteTimeUtc $traceFile.LastWriteTimeUtc) {
    return $null
  }

  $taskId = [string](Get-ObjectPropertyValue -InputObject $trace -Name "task_id" -Default "unknown")
  $failedStage = [string](Get-ObjectPropertyValue -InputObject $trace -Name "failed_stage" -Default "")
  $blockedReason = [string](Get-ObjectPropertyValue -InputObject $trace -Name "blocked_reason" -Default "")
  $relativeTrace = Get-RelativePathCompat -Root $Root -Path $traceFile.FullName
  $summary = "Task $taskId ended with status=$status"
  if (-not [string]::IsNullOrWhiteSpace($failedStage)) {
    $summary += ", failed_stage=$failedStage"
  }
  if (-not [string]::IsNullOrWhiteSpace($blockedReason)) {
    $summary += ", blocked_reason=$blockedReason"
  }

  $targetDoc = "docs/harness/governance-auto-repair.md"
  if ($failedStage -match "stage1|spec|design") {
    $targetDoc = "docs/harness/rule-governance.md"
  }
  elseif ($failedStage -match "stage2|review|test|verify") {
    $targetDoc = "docs/testing/failure-triage.md"
  }

  $hashInput = "$relativeTrace|$summary"
  $bytes = [System.Text.Encoding]::UTF8.GetBytes($hashInput)
  $sha = [System.Security.Cryptography.SHA256]::Create()
  try {
    $hash = ([System.BitConverter]::ToString($sha.ComputeHash($bytes))).Replace("-", "").Substring(0, 16).ToLowerInvariant()
  }
  finally {
    $sha.Dispose()
  }

  return [pscustomobject]@{
    hash = $hash
    task_id = $taskId
    status = $status
    failed_stage = $failedStage
    blocked_reason = $blockedReason
    trace = $relativeTrace
    summary = $summary
    target_doc = $targetDoc
  }
}

function Write-ExperienceEntry {
  param(
    [string]$Root,
    [object]$Experience
  )

  if ($null -eq $Experience) {
    return $false
  }

  $docPath = Join-Path $Root "docs\harness\governance-auto-repair.md"
  $docDir = Split-Path -Parent $docPath
  if (-not (Test-Path -LiteralPath $docDir)) {
    New-Item -ItemType Directory -Force -Path $docDir | Out-Null
  }

  $marker = "<!-- stop-hook-experience:$($Experience.hash) -->"
  if (Test-Path -LiteralPath $docPath) {
    $existing = Get-Content -LiteralPath $docPath -Raw
    if ($existing.Contains($marker)) {
      return $false
    }
  }
  else {
    @"
# Governance Auto Repair

本文件由 Stop hook 治理检查维护，用于记录自动化闭环中反复出现的失败原因、修复去向和后续规则提升候选。这里不是硬规则入口；只有经过复验和维护者确认后，才可提升到 `AGENTS.md`、`.codex/rules/*.md`、prompt 或 `.rules`。

"@ | Set-Content -LiteralPath $docPath -Encoding UTF8
  }

  $entry = @"

## Stop Hook Experience - $($Experience.task_id)

$marker

- Time: $(Get-Date -Format o)
- Summary: $($Experience.summary)
- Trace: `$($Experience.trace)`
- Suggested target doc: `$($Experience.target_doc)`
- Next action: review the trace, repair the root cause, then promote only reusable lessons to the target doc.

"@

  Add-Content -LiteralPath $docPath -Value $entry -Encoding UTF8
  return $true
}

try {
  $resolvedRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path
  $findings = New-Object System.Collections.Generic.List[object]
  $governancePolicy = Get-GovernancePolicy -Root $resolvedRoot

  $moduleAgentsUpdates = Sync-ModuleAgents -Root $resolvedRoot -Findings $findings
  Test-MergeMarkers -Root $resolvedRoot -Findings $findings
  Test-HookSurface -Root $resolvedRoot -Findings $findings
  Test-RulesSurface -Root $resolvedRoot -Findings $findings
  Test-ConfigSurface -Root $resolvedRoot -Findings $findings

  $experience = New-ExperienceFromTrace -Root $resolvedRoot
  $experienceWritten = $false
  if ($WriteExperience -and $null -ne $experience) {
    $experienceWritten = Write-ExperienceEntry -Root $resolvedRoot -Experience $experience
  }

  if ($null -ne $experience) {
    $traceSeverity = if ([string]$governancePolicy.failed_trace_mode -eq "block") { "block" } else { "warn" }
    $traceRepairMode = if ([string]$governancePolicy.failed_trace_mode -eq "debt") { "soft_debt" } else { "auto_repair" }
    Add-Finding `
      -Findings $findings `
      -Severity $traceSeverity `
      -Category "failed_trace_experience" `
      -Message $experience.summary `
      -RepairMode $traceRepairMode `
      -TargetDoc $experience.target_doc
  }

  $hasBlockingFinding = @($findings | Where-Object { $_.severity -eq "block" }).Count -gt 0
  $status = if ($hasBlockingFinding) { "fail" } else { "pass" }

  [pscustomobject]@{
    status = $status
    findings = @($findings.ToArray())
    experience = $experience
    experience_written = $experienceWritten
    module_agents_updates = @($moduleAgentsUpdates)
    next_action = "Read AGENTS.md, docs/harness/governance-auto-repair.md, docs/harness/rule-governance.md, .codex/rules/agents.md, and $($governancePolicy.decision_log); auto-resolve ambiguities, log rationale, prefer $($governancePolicy.env_fallback) bootstrap for missing services or commands, then rerun tools/harness/verify.ps1."
  } | ConvertTo-Json -Depth 8 -Compress
}
catch {
  $line = if ($null -ne $_.InvocationInfo) { [string]$_.InvocationInfo.ScriptLineNumber } else { "" }
  [pscustomobject]@{
    status = "fail"
    findings = @([pscustomobject]@{
      severity = "block"
      category = "governance_check_error"
      message = if ([string]::IsNullOrWhiteSpace($line)) { $_.Exception.Message } else { "$($_.Exception.Message) at line $line" }
      repair_mode = "agent_repair"
      target_doc = "docs/harness/governance-auto-repair.md"
    })
    experience = $null
    experience_written = $false
    module_agents_updates = @()
    next_action = "Inspect scripts/harness/harness-governance-check.ps1 and rerun tools/harness/verify.ps1."
  } | ConvertTo-Json -Depth 8 -Compress
}
