extensions/specrew-speckit/scripts/scaffold-iteration-artifacts.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$SpecDirectory,

    [Parameter(Mandatory = $true)]
    [string]$IterationNumber,

    [switch]$DryRun,
    [switch]$PassThru
)

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

function Add-ScaffoldAction {
    param(
        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions,

        [Parameter(Mandatory = $true)]
        [string]$Action,

        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $null = $Actions.Add([pscustomobject]@{
            Action = $Action
            Path   = $Path
        })
}

function Ensure-Directory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (Test-Path -LiteralPath $Path) {
        Add-ScaffoldAction -Actions $Actions -Action 'preserved-directory' -Path $Path
        return
    }

    Add-ScaffoldAction -Actions $Actions -Action $(if ($DryRun) { 'would-create-directory' } else { 'created-directory' }) -Path $Path
    if (-not $DryRun) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Write-MissingFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [Parameter(Mandatory = $true)]
        [string]$Content,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (Test-Path -LiteralPath $TargetPath) {
        Add-ScaffoldAction -Actions $Actions -Action 'preserved' -Path $TargetPath
        return
    }

    Add-ScaffoldAction -Actions $Actions -Action $(if ($DryRun) { 'would-create' } else { 'created' }) -Path $TargetPath
    if (-not $DryRun) {
        $parent = Split-Path -Parent $TargetPath
        if (-not (Test-Path -LiteralPath $parent)) {
            New-Item -ItemType Directory -Path $parent -Force | Out-Null
        }

        [System.IO.File]::WriteAllText($TargetPath, $Content, [System.Text.UTF8Encoding]::new($false))
    }
}

function Get-MarkdownContent {
    param([string]$Path)

    return @(Get-Content -LiteralPath $Path -Encoding UTF8)
}

function Get-MarkdownSectionTable {
    param(
        [AllowNull()]
        [AllowEmptyCollection()]
        [string[]]$Lines,
        [string]$Heading
    )

    if ($null -eq $Lines -or $Lines.Count -eq 0) {
        return @()
    }

    $headingPattern = '^#{2,3}\s+' + [regex]::Escape($Heading) + '\b'
    $startIndex = -1

    for ($index = 0; $index -lt $Lines.Count; $index++) {
        if ($Lines[$index] -match $headingPattern) {
            $startIndex = $index
            break
        }
    }

    if ($startIndex -lt 0) {
        return @()
    }

    $tableLines = New-Object System.Collections.Generic.List[string]
    for ($index = $startIndex + 1; $index -lt $Lines.Count; $index++) {
        $currentLine = $Lines[$index]
        if ($currentLine -match '^#{2,3}\s+') {
            break
        }

        if ($currentLine.Trim().StartsWith('|')) {
            $null = $tableLines.Add($currentLine)
        }
    }

    if ($tableLines.Count -lt 2) {
        return @()
    }

    $headers = ($tableLines[0].Trim('|') -split '\|') | ForEach-Object { $_.Trim() }
    $rows = New-Object System.Collections.Generic.List[object]

    for ($rowIndex = 1; $rowIndex -lt $tableLines.Count; $rowIndex++) {
        $cells = ($tableLines[$rowIndex].Trim('|') -split '\|') | ForEach-Object { $_.Trim() }
        $isSeparator = $true

        foreach ($cell in $cells) {
            if ($cell -notmatch '^:?-{3,}:?$') {
                $isSeparator = $false
                break
            }
        }

        if ($isSeparator) {
            continue
        }

        $row = [ordered]@{}
        for ($cellIndex = 0; $cellIndex -lt $headers.Count; $cellIndex++) {
            $value = if ($cellIndex -lt $cells.Count) { $cells[$cellIndex] } else { '' }
            $row[$headers[$cellIndex]] = $value
        }

        $rows.Add([pscustomobject]$row)
    }

    return $rows.ToArray()
}

function Get-MarkdownMetadataValue {
    param(
        [string[]]$Lines,
        [string]$Label
    )

    $pattern = '^\*\*' + [regex]::Escape($Label) + '\*\*:\s*(.+?)\s*$'
    foreach ($line in $Lines) {
        if ($line -match $pattern) {
            return $Matches[1].Trim()
        }
    }

    return $null
}

function Normalize-MarkdownCell {
    param([AllowNull()][string]$Value)

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

    return $Value.Trim().Trim('`')
}

function Convert-ToRepoRelativePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$BasePath,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath
    )

    return ([System.IO.Path]::GetRelativePath($BasePath, $TargetPath)).Replace('\', '/')
}

function Get-DefaultRequirementRefsForGate {
    param(
        [Parameter(Mandatory = $true)]
        [string]$GateId
    )

    switch ($GateId) {
        'dead-field' { return @('FR-011', 'FR-027', 'FR-030') }
        'anti-pattern' { return @('FR-011', 'FR-028', 'FR-030') }
        'test-integrity' { return @('FR-011', 'FR-029', 'FR-030') }
        'stack-tooling-evidence' { return @('FR-011') }
        'quality-lens-review' { return @('FR-011', 'FR-012') }
        'concurrency-correctness-review' { return @('FR-011', 'FR-012', 'FR-015') }
        'resiliency-semantics-review' { return @('FR-011', 'FR-012', 'FR-015') }
        'retry-idempotency-review' { return @('FR-011', 'FR-012', 'FR-015') }
        default { return @('FR-011') }
    }
}

function Resolve-QualityEvidenceSource {
    param(
        [AllowNull()][string]$Value,
        [Parameter(Mandatory = $true)]
        [string]$FeatureId,
        [Parameter(Mandatory = $true)]
        [string]$IterationNumber,
        [Parameter(Mandatory = $true)]
        [string]$FindingsRef,
        [Parameter(Mandatory = $true)]
        [string]$EvidenceRef
    )

    $normalized = Normalize-MarkdownCell $Value
    if ([string]::IsNullOrWhiteSpace($normalized)) {
        return $EvidenceRef
    }

    $resolved = $normalized.Replace('specs/<feature>/iterations/<NNN>/quality/mechanical-findings.json', $FindingsRef)
    $resolved = $resolved.Replace('specs/<feature>/iterations/<NNN>/quality/quality-evidence.md', $EvidenceRef)
    $resolved = $resolved.Replace('<feature>', $FeatureId)
    $resolved = $resolved.Replace('<NNN>', $IterationNumber)
    return $resolved
}

function Get-DefaultQualityGateRows {
    return @(
        [pscustomobject]@{ 'Required Quality Gate' = 'dead-field'; Category = 'mechanical'; 'Evidence Source' = 'specs/<feature>/iterations/<NNN>/quality/mechanical-findings.json' }
        [pscustomobject]@{ 'Required Quality Gate' = 'anti-pattern'; Category = 'mechanical'; 'Evidence Source' = 'specs/<feature>/iterations/<NNN>/quality/mechanical-findings.json' }
        [pscustomobject]@{ 'Required Quality Gate' = 'test-integrity'; Category = 'mechanical'; 'Evidence Source' = 'specs/<feature>/iterations/<NNN>/quality/mechanical-findings.json' }
        [pscustomobject]@{ 'Required Quality Gate' = 'stack-tooling-evidence'; Category = 'tooling'; 'Evidence Source' = 'specs/<feature>/iterations/<NNN>/quality/quality-evidence.md' }
        [pscustomobject]@{ 'Required Quality Gate' = 'quality-lens-review'; Category = 'manual-evidence'; 'Evidence Source' = 'specs/<feature>/iterations/<NNN>/quality/quality-evidence.md' }
    )
}

function Get-QualityEvidenceContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$PlanPath,
        [Parameter(Mandatory = $true)]
        [string]$FeatureId,
        [Parameter(Mandatory = $true)]
        [string]$IterationNumber,
        [Parameter(Mandatory = $true)]
        [string]$FindingsRef,
        [Parameter(Mandatory = $true)]
        [string]$EvidenceRef,
        [Parameter(Mandatory = $true)]
        [string]$ReviewedAt
    )

    $planLines = if (Test-Path -LiteralPath $PlanPath -PathType Leaf) {
        @(Get-MarkdownContent -Path $PlanPath)
    }
    else {
        @()
    }

    $profileRef = Normalize-MarkdownCell (Get-MarkdownMetadataValue -Lines $planLines -Label 'Inferred Quality Profile')
    if ([string]::IsNullOrWhiteSpace($profileRef)) {
        $profileRef = 'quality-profile.pending'
    }

    $presetRefs = Normalize-MarkdownCell (Get-MarkdownMetadataValue -Lines $planLines -Label 'Selected preset ref or explicit custom composition')
    if ([string]::IsNullOrWhiteSpace($presetRefs)) {
        $presetRefs = '(pending preset selection)'
    }

    $gateRows = @(Get-MarkdownSectionTable -Lines $planLines -Heading 'Required Quality Gates')
    if ($gateRows.Count -eq 0) {
        $gateRows = @(Get-DefaultQualityGateRows)
    }
    $lines = [System.Collections.Generic.List[string]]::new()
    $null = $lines.Add("# Quality Evidence: Iteration $IterationNumber")
    $null = $lines.Add('')
    $null = $lines.Add(('**Profile Ref**: `' + $profileRef + '`'))
    $null = $lines.Add(('**Preset Refs**: ' + $presetRefs))
    $null = $lines.Add(('**Findings Ref**: `' + $FindingsRef + '`'))
    $null = $lines.Add('**Reviewed By**: Reviewer (pending)')
    $null = $lines.Add(('**Reviewed At**: ' + $ReviewedAt))
    $null = $lines.Add('')
    $null = $lines.Add('## Gate Matrix')
    $null = $lines.Add('')
    $null = $lines.Add('| Gate | Requirement | Evidence Source | Status | Exception |')
    $null = $lines.Add('| --- | --- | --- | --- | --- |')

    foreach ($gateRow in $gateRows) {
        $gateId = Normalize-MarkdownCell ([string]$gateRow.'Required Quality Gate')
        if ([string]::IsNullOrWhiteSpace($gateId)) {
            continue
        }

        $requirementRefs = (Get-DefaultRequirementRefsForGate -GateId $gateId) -join ', '
        $evidenceSource = Resolve-QualityEvidenceSource `
            -Value ([string]$gateRow.'Evidence Source') `
            -FeatureId $FeatureId `
            -IterationNumber $IterationNumber `
            -FindingsRef $FindingsRef `
            -EvidenceRef $EvidenceRef

        $null = $lines.Add(('| `{0}` | {1} | `{2}` | `planned` | `—` |' -f $gateId, $requirementRefs, $evidenceSource))
    }

    return ($lines -join [Environment]::NewLine) + [Environment]::NewLine
}

function Get-MechanicalFindingsScaffoldJson {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FeatureRef,
        [Parameter(Mandatory = $true)]
        [string]$IterationRef,
        [Parameter(Mandatory = $true)]
        [string]$GeneratedAt
    )

    return ([pscustomobject][ordered]@{
            schemaVersion = 'v1'
            featureRef    = $FeatureRef
            iterationRef  = $IterationRef
            generatedAt   = $GeneratedAt
            generator     = [pscustomobject][ordered]@{
                name    = 'specrew-iteration-scaffold'
                version = '1.0.0'
            }
            findings      = @()
        } | ConvertTo-Json -Depth 8)
}

function Get-MechanicalFindingsSchemaJson {
    # JSON Schema for `iterations/<NNN>/quality/mechanical-findings.json` as written by
    # `run-mechanical-checks.ps1`. Lives at the FEATURE level (`<feature>/contracts/
    # mechanical-findings.schema.json`) because it's a stable contract across iterations.
    #
    # Empirical motivation: tip-calc-v2 dogfooding 2026-05-23 caught
    # `run-mechanical-checks.ps1` throwing "Mechanical findings schema not found at
    # '<feature>/contracts/mechanical-findings.schema.json'" because the scaffold
    # never created it. Claude hand-authored it to match the runner's documented v1
    # output (logged as drift D-001). Now the scaffold writes it proactively so the
    # runner works on first invocation.
    return @'
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Specrew Mechanical Findings (v1)",
  "description": "Schema for iterations/<NNN>/quality/mechanical-findings.json emitted by run-mechanical-checks.ps1 (dead-field, anti-pattern, test-integrity lenses).",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schemaVersion",
    "featureRef",
    "iterationRef",
    "generatedAt",
    "generator",
    "findings"
  ],
  "properties": {
    "schemaVersion": { "type": "string", "enum": ["v1"] },
    "featureRef": { "type": "string" },
    "iterationRef": { "type": "string" },
    "generatedAt": { "type": "string" },
    "generator": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "properties": {
        "name": { "type": "string" },
        "version": { "type": "string" }
      }
    },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "findingId",
          "gateId",
          "ruleId",
          "surfaceId",
          "severity",
          "message",
          "remediation",
          "source",
          "requirementRefs",
          "demoted"
        ],
        "properties": {
          "findingId": { "type": "string" },
          "gateId": {
            "type": "string",
            "enum": ["dead-field", "anti-pattern", "test-integrity"]
          },
          "ruleId": { "type": "string" },
          "surfaceId": { "type": "string" },
          "severity": { "type": "string" },
          "message": { "type": "string" },
          "remediation": { "type": "string" },
          "source": {
            "type": "object",
            "additionalProperties": true,
            "required": ["path", "line"],
            "properties": {
              "path": { "type": "string" },
              "line": { "type": "integer" },
              "column": { "type": "integer" }
            }
          },
          "requirementRefs": {
            "type": "array",
            "items": { "type": "string" }
          },
          "demoted": { "type": "boolean" },
          "dispositionRef": { "type": "string" }
        }
      }
    }
  }
}
'@

}

function Test-PhaseTwoQualityArtifactScaffold {
    param(
        [AllowEmptyCollection()]
        [string[]]$PlanLines,
        [string]$QualityContractPath
    )

    $phaseTwoPattern = 'hardening-gate\.md|trap-reapplication\.md|quality[\\/]+lenses'
    $planText = ($PlanLines -join [Environment]::NewLine)
    if ($planText -match $phaseTwoPattern) {
        return $true
    }

    if (Test-Path -LiteralPath $QualityContractPath -PathType Leaf) {
        $contractText = Get-Content -LiteralPath $QualityContractPath -Raw -Encoding UTF8
        return $contractText -match $phaseTwoPattern
    }

    return $false
}

function Get-HardeningGateContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FeatureRef,
        [Parameter(Mandatory = $true)]
        [string]$IterationRef,
        [Parameter(Mandatory = $true)]
        [string]$IterationNumber,
        [Parameter(Mandatory = $true)]
        [string]$ReviewedAt
    )

    $lines = [System.Collections.Generic.List[string]]::new()
    $null = $lines.Add("# Hardening Gate: Iteration $IterationNumber")
    $null = $lines.Add('')
    $null = $lines.Add('**Schema**: v1')
    $null = $lines.Add('**Gate ID**: `pre-implementation-hardening`')
    $null = $lines.Add(('**Feature Ref**: `' + $FeatureRef + '`'))
    $null = $lines.Add(('**Iteration Ref**: `' + $IterationRef + '`'))
    $null = $lines.Add('**Requested Review Class**: `strongest-available`')
    $null = $lines.Add('**Effective Review Class**: `(pending hardening review)`')
    $null = $lines.Add('**Overall Verdict**: `blocked`')
    $null = $lines.Add('**Approval Ref**: `—`')
    $null = $lines.Add('**Reviewed By**: Reviewer (pending)')
    $null = $lines.Add(('**Reviewed At**: ' + $ReviewedAt))
    $null = $lines.Add('')
    $null = $lines.Add('<!--')
    $null = $lines.Add(' Concern Review schema (validator-enforced):')
    $null = $lines.Add(' - Status MUST be one of: `addressed` | `not-applicable` | `deferred-with-approval`. The validator')
    $null = $lines.Add(' rejects placeholder values like `tbd`. Pick a real status per concern before implementation.')
    $null = $lines.Add(' - When Status is `addressed`: EvidenceBasis = `planning-time-analysis`, RuntimeEvidenceStatus =')
    $null = $lines.Add(' `pending-post-implementation`, ExpectedControls = concrete controls you will enforce.')
    $null = $lines.Add(' - When Status is `not-applicable`: EvidenceBasis = `not-applicable`, RuntimeEvidenceStatus =')
    $null = $lines.Add(' `not-needed`, ExpectedControls = `—`. Rationale must explain WHY this concern does not apply.')
    $null = $lines.Add(' - When Status is `deferred-with-approval`: same evidence fields as `addressed`, AND the Approval')
    $null = $lines.Add(' column must reference an approval record (decision or defer) with a recorded human approval.')
    $null = $lines.Add(' - Overall Verdict is computed: `ready` when every concern is addressed/not-applicable/deferred-')
    $null = $lines.Add(' with-approval; `blocked` otherwise. Update the metadata above when you change the table.')
    $null = $lines.Add('-->')
    $null = $lines.Add('')
    $null = $lines.Add('## Concern Review')
    $null = $lines.Add('')
    $null = $lines.Add('| Concern | Category | Status | Evidence Basis | Runtime Evidence Status | Expected Controls | Blocking | Rationale | Approval |')
    $null = $lines.Add('| --- | --- | --- | --- | --- | --- | --- | --- | --- |')
    $null = $lines.Add('| `security-surface` | `security` | `addressed` | `planning-time-analysis` | `pending-post-implementation` | `<list concrete controls: input validation, allowlists, no eval/innerHTML on user data, no persistence APIs unless required, etc.>` | `true` | `<describe the trust boundary, privilege model, and sensitive flows in this iteration>` | `—` |')
    $null = $lines.Add('| `error-handling-expectations` | `robustness` | `addressed` | `planning-time-analysis` | `pending-post-implementation` | `<list failure modes and the single transition path that handles them, plus positive + negative test coverage you will assert>` | `true` | `<describe expected failure semantics, incomplete-state handling, and recovery preservation rules>` | `—` |')
    $null = $lines.Add('| `retry-idempotency-requirements` | `resilience` | `not-applicable` | `not-applicable` | `not-needed` | `—` | `false` | `<flip to `addressed` and fill in if this iteration has retries, idempotency keys, transactional state, or shared resources. Otherwise record the rationale for why those primitives have no surface here.>` | `—` |')
    $null = $lines.Add('| `test-integrity-targets` | `verification` | `addressed` | `planning-time-analysis` | `pending-post-implementation` | `<list the FR → named-test mapping, the negative-path requirements, and which evidence artifacts will record empirical results>` | `true` | `<describe coverage strategy: positive + negative per FR; smoke-only is disallowed for failure-mode FRs>` | `—` |')
    $null = $lines.Add('| `operational-resilience-concerns` | `operability` | `not-applicable` | `not-applicable` | `not-needed` | `—` | `false` | `<flip to `addressed` and fill in if this iteration ships server, SLO, telemetry pipeline, oncall surface, or operational dependencies. Otherwise record the rationale for why those primitives have no surface here.>` | `—` |')
    $null = $lines.Add('')
    $null = $lines.Add('## Lens Activation (Planning Baseline)')
    $null = $lines.Add('')
    $null = $lines.Add('| Lens Ref | Activation | Planned Evidence Path |')
    $null = $lines.Add('| --- | --- | --- |')
    $null = $lines.Add(('| `security-baseline@v1.0.0` | required | `' + $IterationRef + '/quality/lenses/security-baseline.md` |'))
    $null = $lines.Add(('| `robustness-baseline@v1.0.0` | required | `' + $IterationRef + '/quality/lenses/robustness-baseline.md` |'))
    $null = $lines.Add(('| `test-integrity@v1.0.0` | required | `' + $IterationRef + '/quality/lenses/test-integrity.md` |'))
    $null = $lines.Add('')
    $null = $lines.Add('## Notes')
    $null = $lines.Add('')
    $null = $lines.Add('- Replace every `<placeholder>` and every angle-bracketed instruction with iteration-specific content before crossing the `before-implement` boundary.')
    $null = $lines.Add('- After every row in the table is filled in with a canonical Status, flip the metadata `Overall Verdict` to `ready` (if every concern is `addressed` / `not-applicable` / `deferred-with-approval`) or keep `blocked`.')
    $null = $lines.Add('- Runtime evidence (lens execution, test counts, mechanical-findings results) is collected after implementation lands; the gate is a PLANNING-time artifact and that deferral is intentional.')
    return ($lines -join [Environment]::NewLine) + [Environment]::NewLine
}

function Get-TrapReapplicationContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$IterationNumber,
        [Parameter(Mandatory = $true)]
        [string]$RecordedAt
    )

    $lines = [System.Collections.Generic.List[string]]::new()
    $null = $lines.Add("# Trap Reapplication: Iteration $IterationNumber")
    $null = $lines.Add('')
    $null = $lines.Add('**Schema**: v1')
    $null = $lines.Add('**Scan ID**: `trap-reapplication.pending`')
    $null = $lines.Add(('**Recorded At**: ' + $RecordedAt))
    $null = $lines.Add('')
    $null = $lines.Add('## Scan Log')
    $null = $lines.Add('')
    $null = $lines.Add('| Trap Ref | Scan Scope | Result | Matches |')
    $null = $lines.Add('| --- | --- | --- | --- |')
    $null = $lines.Add('| `(pending trap refs)` | `(pending scan scope)` | `skipped-with-rationale` | Scaffolded placeholder. Known-trap reapplication has not run yet. |')
    $null = $lines.Add('')
    $null = $lines.Add('## Notes')
    $null = $lines.Add('')
    $null = $lines.Add('- Replace the placeholder row with concrete trap scan evidence once reapplication runs.')
    return ($lines -join [Environment]::NewLine) + [Environment]::NewLine
}

function Get-PlanTaskIds {
    param(
        [Parameter(Mandatory = $true)]
        [string]$PlanPath
    )

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

    $planLines = @(Get-MarkdownContent -Path $PlanPath)
    $taskRows = @(Get-MarkdownSectionTable -Lines $planLines -Heading 'Tasks')
    $seen = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
    $taskIds = New-Object System.Collections.Generic.List[string]

    foreach ($taskRow in $taskRows) {
        $taskId = [string]$taskRow.Task
        if ([string]::IsNullOrWhiteSpace($taskId)) {
            continue
        }

        $taskId = $taskId.Trim()
        if ($seen.Add($taskId)) {
            $null = $taskIds.Add($taskId)
        }
    }

    return $taskIds.ToArray()
}

function Get-BaselineRef {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SpecDirectory
    )

    $projectRoot = Split-Path -Parent $SpecDirectory
    $defaultBaselineRef = 'iteration-baseline'
    $configPath = Join-Path $projectRoot '.specrew\iteration-config.yml'

    if (Test-Path -LiteralPath $configPath -PathType Leaf) {
        foreach ($line in Get-Content -LiteralPath $configPath -Encoding UTF8) {
            if ($line -match '^\s{2}baseline_ref:\s*"?([^"#]+?)"?\s*$') {
                $defaultBaselineRef = $Matches[1].Trim()
                break
            }
        }
    }

    $gitCommand = Get-Command -Name 'git' -ErrorAction SilentlyContinue
    if ($null -eq $gitCommand) {
        return $defaultBaselineRef
    }

    $headRef = @(& git -C $projectRoot rev-parse HEAD 2>$null)
    $gitExit = $LASTEXITCODE
    # Reset $LASTEXITCODE so the caller doesn't inherit our git exit code. In greenfield-new projects
    # `git rev-parse HEAD` returns 128 (no commits yet); without this reset the surrounding script
    # exits 128 even though we successfully fell back to the default baseline ref.
    $global:LASTEXITCODE = 0
    if ($gitExit -eq 0 -and $headRef.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($headRef[0])) {
        return [string]$headRef[0]
    }

    return $defaultBaselineRef
}

$resolvedSpecDirectory = [System.IO.Path]::GetFullPath($SpecDirectory)
$projectRoot = Split-Path -Parent (Split-Path -Parent $resolvedSpecDirectory)
$iterationsRoot = Join-Path $resolvedSpecDirectory 'iterations'
$iterationDirectory = Join-Path $iterationsRoot $IterationNumber
$planPath = Join-Path $iterationDirectory 'plan.md'
$statePath = Join-Path $iterationDirectory 'state.md'
$driftLogPath = Join-Path $iterationDirectory 'drift-log.md'
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$actions = [System.Collections.ArrayList]::new()

if (-not (Test-Path -LiteralPath $resolvedSpecDirectory)) {
    throw "Spec directory '$resolvedSpecDirectory' does not exist."
}

Ensure-Directory -Path $iterationsRoot -Actions $actions
Ensure-Directory -Path $iterationDirectory -Actions $actions

$taskIds = @(Get-PlanTaskIds -PlanPath $planPath)
$tasksRemaining = if ($taskIds.Count -gt 0) { $taskIds -join ', ' } else { '(populate from plan.md)' }
$baselineRef = Get-BaselineRef -SpecDirectory $resolvedSpecDirectory

$stateContent = @"
# Iteration State: $IterationNumber
 
**Schema**: v1
**Last Completed Task**: (none)
**Tasks Remaining**: $tasksRemaining
**In Progress**: (none)
**Baseline Ref**: $baselineRef
**Updated**: $timestamp
 
## Execution Summary
 
- Execution has not started yet.
- This artifact was scaffolded before task execution so resume state can be updated after each task.
 
## Notes
 
- Update this file after each task completes.
- Keep task identifiers aligned to plan.md.
 
<!-- >>> specrew-managed escalation-state >>> -->
## Repair Escalation
 
- **Status**: inactive
- **Artifact**: (none)
- **Gate**: (none)
- **Failure Count**: 0
- **Current Tier**: efficiency
- **Current Owner**: (none)
- **Locked Out Agents**: (none)
- **Last Escalated**: (none)
- **Resolved At**: (none)
- **Notes**: (none)
<!-- <<< specrew-managed escalation-state <<< -->
"@


$driftLogContent = @"
# Drift Log: Iteration $IterationNumber
 
**Schema**: v1
 
<!--
  Markdown authoring note (Specrew lifecycle convention):
 
  When you add new drift events to this file, watch for MD032 (blanks-around-lists).
  A sentence ending with a colon, immediately followed by a bullet list, is the most
  common violation. Always put a BLANK LINE between the colon line and the list:
 
      BAD: GOOD:
      Resolution steps: Resolution steps:
      - Step one <— blank line here
      - Step two - Step one
                                        - Step two
 
  The F-033 pre-boundary markdownlint gate runs `markdownlint-cli --fix` on .md
  changes before every boundary-sync write, so most violations auto-fix — but the
  blank line you write in the first place avoids the cleanup churn.
-->
 
## Summary
 
**Total drift events**: 0
**Resolution rate**: 100% (0/0 resolved)
**Specification drift**: None detected
 
## Events
 
No specification drift detected during Iteration $IterationNumber execution to date.
 
### Resolution Strategies (Unused)
 
The following resolution strategies remain available if drift is detected later in execution:
 
- **spec-updated**: Update the spec to reflect implementation choice
- **implementation-reverted**: Revert implementation to match spec
- **deferred**: Mark drift as deferred to next iteration
- **human-decision**: Escalate to Alon for resolution
 
### Notes
 
- This artifact was scaffolded before review starts so drift can be logged immediately when detected.
- Replace the zero-drift summary with real counts when the first drift event is recorded.
"@


Write-MissingFile -TargetPath $statePath -Content $stateContent -Actions $actions
Write-MissingFile -TargetPath $driftLogPath -Content $driftLogContent -Actions $actions

$planLines = if (Test-Path -LiteralPath $planPath -PathType Leaf) {
    @(Get-MarkdownContent -Path $planPath)
}
else {
    @()
}

# F-040 dogfooding fix (tip-calc-v2 + codex-test, 2026-05-24): unconditionally write the
# feature-level contracts/mechanical-findings.schema.json so run-mechanical-checks.ps1
# works on first invocation. Previously Claude had to hand-author this on encountering
# the runner's "Mechanical findings schema not found" error (logged as drift D-001).
$featureContractsDirectory = Join-Path $resolvedSpecDirectory 'contracts'
Ensure-Directory -Path $featureContractsDirectory -Actions $actions
$mechanicalSchemaPath = Join-Path $featureContractsDirectory 'mechanical-findings.schema.json'
Write-MissingFile -TargetPath $mechanicalSchemaPath -Content (Get-MechanicalFindingsSchemaJson) -Actions $actions

$qualityContractPath = Join-Path $resolvedSpecDirectory 'contracts\quality-governance-artifacts.md'
$qualityGateRows = @(Get-MarkdownSectionTable -Lines $planLines -Heading 'Required Quality Gates')
$hasQualityEvidenceContract = $qualityGateRows.Count -gt 0 -or (Test-Path -LiteralPath $qualityContractPath -PathType Leaf)
$phaseTwoQualityArtifactsRequired = Test-PhaseTwoQualityArtifactScaffold -PlanLines $planLines -QualityContractPath $qualityContractPath
if ($hasQualityEvidenceContract -or $phaseTwoQualityArtifactsRequired) {
    $qualityDirectory = Join-Path $iterationDirectory 'quality'
    $featureId = Split-Path -Leaf $resolvedSpecDirectory
    $featureRef = Convert-ToRepoRelativePath -BasePath $projectRoot -TargetPath (Join-Path $resolvedSpecDirectory 'spec.md')
    $iterationRef = Convert-ToRepoRelativePath -BasePath $projectRoot -TargetPath $iterationDirectory
    $hardeningGatePath = Join-Path $qualityDirectory 'hardening-gate.md'
    $lensesDirectory = Join-Path $qualityDirectory 'lenses'
    $trapReapplicationPath = Join-Path $qualityDirectory 'trap-reapplication.md'
    $qualityEvidencePath = Join-Path $qualityDirectory 'quality-evidence.md'
    $mechanicalFindingsPath = Join-Path $qualityDirectory 'mechanical-findings.json'
    $qualityEvidenceRef = Convert-ToRepoRelativePath -BasePath $projectRoot -TargetPath $qualityEvidencePath
    $mechanicalFindingsRef = Convert-ToRepoRelativePath -BasePath $projectRoot -TargetPath $mechanicalFindingsPath

    Ensure-Directory -Path $qualityDirectory -Actions $actions
    if ($phaseTwoQualityArtifactsRequired) {
        Ensure-Directory -Path $lensesDirectory -Actions $actions
        Write-MissingFile -TargetPath $hardeningGatePath -Content (Get-HardeningGateContent `
                -FeatureRef $featureRef `
                -IterationRef $iterationRef `
                -IterationNumber $IterationNumber `
                -ReviewedAt $timestamp) -Actions $actions
        Write-MissingFile -TargetPath $trapReapplicationPath -Content (Get-TrapReapplicationContent `
                -IterationNumber $IterationNumber `
                -RecordedAt $timestamp) -Actions $actions
    }

    if ($hasQualityEvidenceContract) {
        Write-MissingFile -TargetPath $qualityEvidencePath -Content (Get-QualityEvidenceContent `
                -PlanPath $planPath `
                -FeatureId $featureId `
                -IterationNumber $IterationNumber `
                -FindingsRef $mechanicalFindingsRef `
                -EvidenceRef $qualityEvidenceRef `
                -ReviewedAt $timestamp) -Actions $actions
        Write-MissingFile -TargetPath $mechanicalFindingsPath -Content (Get-MechanicalFindingsScaffoldJson `
                -FeatureRef $featureRef `
                -IterationRef $iterationRef `
                -GeneratedAt $timestamp) -Actions $actions
    }
}

if ($PassThru) {
    $actions
    return
}

$actions | Select-Object Action, Path | Format-Table -AutoSize
Write-Host ("Iteration artifact scaffold {0} for {1}" -f ($(if ($DryRun) { 'previewed' } else { 'completed' }), $iterationDirectory)) -ForegroundColor Green
exit 0