prizmkit 1.1.70 → 1.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,9 @@ $ErrorActionPreference = 'Stop'
3
3
 
4
4
  $script:PrizmAiProcess = $null
5
5
 
6
- if (-not ('PrizmLineLogBridge' -as [type])) {
6
+ function Initialize-PrizmLineLogBridgeType {
7
+ if ('PrizmLineLogBridge' -as [type]) { return }
8
+
7
9
  Add-Type -TypeDefinition @'
8
10
  using System;
9
11
  using System.Diagnostics;
@@ -164,6 +166,63 @@ function Test-PrizmInfraError {
164
166
  return ($haystack -match '(?i)auth_unavailable|no auth available|502 Bad Gateway|503 Service Unavailable|504 Gateway Timeout|gateway timeout|upstream (connect )?error|connection reset|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|rate limit|rate_limit|temporarily unavailable|overloaded')
165
167
  }
166
168
 
169
+ function Test-PrizmAiRuntimeError {
170
+ param([string]$SessionLog, [string]$ProgressJson)
171
+
172
+ if ($ProgressJson -and (Test-Path $ProgressJson)) {
173
+ try {
174
+ $progress = Get-Content $ProgressJson -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
175
+ if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) {
176
+ return $true
177
+ }
178
+ } catch {}
179
+ }
180
+
181
+ $parts = @()
182
+ if ($SessionLog -and (Test-Path $SessionLog)) {
183
+ try {
184
+ $text = Get-Content $SessionLog -Raw -ErrorAction Stop
185
+ if ($text.Length -gt 65536) { $text = $text.Substring($text.Length - 65536) }
186
+ $parts += $text
187
+ } catch {}
188
+ }
189
+ if ($ProgressJson -and (Test-Path $ProgressJson)) {
190
+ try { $parts += (Get-Content $ProgressJson -Raw -ErrorAction Stop) } catch {}
191
+ }
192
+ if ($parts.Count -eq 0) { return $false }
193
+
194
+ $haystack = $parts -join "`n"
195
+ $contextPattern = '(?i)context_too_large|model_context_window_exceeded|input exceeds the context window|context window of this model|context window (was )?exceeded|exceeded (the )?context window|invalid_request_error.*context window|context window.*invalid_request_error'
196
+ $errorPattern = '(?i)api error|invalid_request_error|api_error_status|api_error_code|status\s*[:=]?\s*(400|413)|last_result_is_error\s*["'':=]?\s*true|is_error\s*["'':=]?\s*true'
197
+ if (($haystack -match $contextPattern) -and ($haystack -match $errorPattern)) { return $true }
198
+ return $false
199
+ }
200
+
201
+ function Get-PrizmProgressFatalErrorCode {
202
+ param([string]$ProgressFile)
203
+ if (-not (Test-Path $ProgressFile)) { return '' }
204
+ try {
205
+ $progress = Get-Content $ProgressFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
206
+ if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) {
207
+ return [string]$progress.fatal_error_code
208
+ }
209
+ } catch {}
210
+ return ''
211
+ }
212
+
213
+ function Write-PrizmFatalErrorMarker {
214
+ param([string]$MarkerPath, [string]$FatalErrorCode, [int]$StaleSeconds, [int]$Threshold)
215
+ $markerDir = Split-Path $MarkerPath -Parent
216
+ if ($markerDir) { New-Item -ItemType Directory -Force -Path $markerDir | Out-Null }
217
+ [ordered]@{
218
+ killed_at = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
219
+ reason = $FatalErrorCode
220
+ fatal_error_code = $FatalErrorCode
221
+ stale_seconds = $StaleSeconds
222
+ threshold = $Threshold
223
+ } | ConvertTo-Json -Compress | Set-Content -Path $MarkerPath -Encoding UTF8
224
+ }
225
+
167
226
  function Get-PrizmConfigValue {
168
227
  param([string]$ConfigPath, [string]$Key)
169
228
  if (-not (Test-Path $ConfigPath)) { return $null }
@@ -638,6 +697,7 @@ function Invoke-PrizmAiSession {
638
697
  $psi.Arguments = $argumentString
639
698
  }
640
699
 
700
+ Initialize-PrizmLineLogBridgeType
641
701
  $process = [System.Diagnostics.Process]::Start($psi)
642
702
  $script:PrizmAiProcess = $process
643
703
  $logBridge = [PrizmLineLogBridge]::new($LogPath)
@@ -335,6 +335,242 @@ function Invoke-PrizmPipeline {
335
335
  return $LASTEXITCODE -eq 0
336
336
  }
337
337
 
338
+ function Get-PrizmFeatureSlugFromList {
339
+ param([string]$ListPath, [string]$FeatureId)
340
+ if (-not (Test-Path $ListPath)) { return '' }
341
+ try { $data = Get-Content $ListPath -Raw | ConvertFrom-Json } catch { return '' }
342
+ foreach ($feature in @($data.features)) {
343
+ if ($feature.id -eq $FeatureId) {
344
+ $number = ([string]$feature.id).Replace('F-', '').Replace('f-', '').PadLeft(3, '0')
345
+ $title = ([string]$feature.title).ToLowerInvariant()
346
+ $title = [regex]::Replace($title, '[^a-z0-9\s-]', '')
347
+ $title = [regex]::Replace($title.Trim(), '[\s]+', '-')
348
+ $title = [regex]::Replace($title, '-+', '-').Trim('-')
349
+ if ($title) { return "$number-$title" }
350
+ return $number
351
+ }
352
+ }
353
+ return ''
354
+ }
355
+
356
+ function Test-PrizmCheckpointComplete {
357
+ param([string]$CheckpointPath)
358
+ if (-not (Test-Path $CheckpointPath)) { return $false }
359
+ try { $checkpoint = Get-Content $CheckpointPath -Raw | ConvertFrom-Json } catch { return $false }
360
+ if (-not $checkpoint.steps) { return $false }
361
+ foreach ($step in @($checkpoint.steps)) {
362
+ if ($step.status -notin @('completed', 'skipped')) { return $false }
363
+ }
364
+ return $true
365
+ }
366
+
367
+ function Get-PrizmFeatureTitleFromList {
368
+ param([string]$ListPath, [string]$FeatureId)
369
+ if (-not (Test-Path $ListPath)) { return '' }
370
+ try { $data = Get-Content $ListPath -Raw | ConvertFrom-Json } catch { return '' }
371
+ foreach ($feature in @($data.features)) {
372
+ if ($feature.id -eq $FeatureId) { return [string]$feature.title }
373
+ }
374
+ return ''
375
+ }
376
+
377
+ function Get-PrizmTitleWords {
378
+ param([string]$Text)
379
+ $matches = [regex]::Matches(([string]$Text).ToLowerInvariant(), '[a-z0-9]{3,}')
380
+ $words = @()
381
+ foreach ($match in $matches) { $words += [string]$match.Value }
382
+ return @($words)
383
+ }
384
+
385
+ function Test-PrizmCommitMatchesFeatureTitle {
386
+ param([string]$Subject, [string]$FeatureTitle)
387
+ $titleWords = @(Get-PrizmTitleWords $FeatureTitle)
388
+ if ($titleWords.Count -eq 0) { return $false }
389
+ $subjectWords = @(Get-PrizmTitleWords $Subject)
390
+ $subjectSet = @{}
391
+ foreach ($word in $subjectWords) { $subjectSet[$word] = $true }
392
+ $required = if ($titleWords.Count -le 3) { $titleWords.Count } else { [Math]::Max(3, [int][Math]::Ceiling($titleWords.Count * 0.75)) }
393
+ $matched = 0
394
+ foreach ($word in $titleWords) {
395
+ if ($subjectSet.ContainsKey($word)) { $matched++ }
396
+ }
397
+ return $matched -ge $required
398
+ }
399
+
400
+ function Get-PrizmFeatureCommit {
401
+ param([string]$ProjectRoot, [string]$BaseCommit, [string]$FeatureId, [bool]$AllowFallback = $false, [string]$FeatureTitle = '')
402
+ $range = if ($BaseCommit) { "$BaseCommit..HEAD" } else { 'HEAD' }
403
+ $lines = & git -C $ProjectRoot log $range '--format=%H%x09%s' 2>$null
404
+ if ($LASTEXITCODE -ne 0) { return '' }
405
+ foreach ($line in @($lines)) {
406
+ $parts = ([string]$line).Split("`t", 2)
407
+ if ($parts.Count -lt 2) { continue }
408
+ $subject = $parts[1]
409
+ if ($subject.Contains($FeatureId) -and $subject -notmatch '^wip(\(|:)') { return $parts[0] }
410
+ }
411
+ if ($AllowFallback -and $FeatureTitle) {
412
+ foreach ($line in @($lines)) {
413
+ $parts = ([string]$line).Split("`t", 2)
414
+ if ($parts.Count -lt 2) { continue }
415
+ $subject = $parts[1]
416
+ if ($subject -notmatch '^wip(\(|:)' -and (Test-PrizmCommitMatchesFeatureTitle $subject $FeatureTitle)) { return $parts[0] }
417
+ }
418
+ }
419
+ return ''
420
+ }
421
+
422
+ function Get-PrizmFeatureSemanticCompletion {
423
+ param([string]$ProjectRoot, [string]$ListPath, [string]$FeatureId, [string]$BaseCommit, [string]$PrizmkitDir)
424
+ $slug = Get-PrizmFeatureSlugFromList $ListPath $FeatureId
425
+ if (-not $slug) { return $null }
426
+ $checkpointPath = Join-Path $PrizmkitDir "specs\$slug\workflow-checkpoint.json"
427
+ if (-not (Test-PrizmCheckpointComplete $checkpointPath)) { return $null }
428
+ $featureTitle = Get-PrizmFeatureTitleFromList $ListPath $FeatureId
429
+ $commitSha = Get-PrizmFeatureCommit $ProjectRoot $BaseCommit $FeatureId $true $featureTitle
430
+ if (-not $commitSha) { return $null }
431
+ return [pscustomobject]@{ Slug = $slug; CommitSha = $commitSha; CheckpointPath = $checkpointPath }
432
+ }
433
+
434
+ function Save-PrizmPostCompletionDirtyArtifacts {
435
+ param([string]$ProjectRoot, [string]$ArtifactDir, [string]$ItemId, [string]$SessionId)
436
+ $status = & git -C $ProjectRoot status --porcelain --untracked-files=all 2>$null
437
+ if ([string]::IsNullOrWhiteSpace(($status -join "`n"))) { return $true }
438
+
439
+ New-Item -ItemType Directory -Force -Path $ArtifactDir | Out-Null
440
+ ($status -join "`n") | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-status.txt') -Encoding UTF8
441
+ & git -C $ProjectRoot diff --binary | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-dirty.patch') -Encoding UTF8
442
+ if ($LASTEXITCODE -ne 0) { return $false }
443
+ & git -C $ProjectRoot diff --cached --binary | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-staged.patch') -Encoding UTF8
444
+ if ($LASTEXITCODE -ne 0) { return $false }
445
+
446
+ $untracked = & git -C $ProjectRoot ls-files --others --exclude-standard 2>$null
447
+ $manifest = Join-Path $ArtifactDir 'post-completion-untracked.txt'
448
+ @($untracked) | Set-Content -Path $manifest -Encoding UTF8
449
+ $untrackedDir = Join-Path $ArtifactDir 'untracked'
450
+ foreach ($rel in @($untracked)) {
451
+ if (-not $rel) { continue }
452
+ $source = Join-Path $ProjectRoot $rel
453
+ $dest = Join-Path $untrackedDir $rel
454
+ $destParent = Split-Path $dest -Parent
455
+ if ($destParent) { New-Item -ItemType Directory -Force -Path $destParent | Out-Null }
456
+ if (Test-Path $source -PathType Leaf) { Copy-Item -LiteralPath $source -Destination $dest -Force }
457
+ elseif (Test-Path $source -PathType Container) { New-Item -ItemType Directory -Force -Path $dest | Out-Null }
458
+ }
459
+
460
+ @(
461
+ '# Post-completion dirty changes preserved',
462
+ '',
463
+ "- Feature: $ItemId",
464
+ "- Session: $SessionId",
465
+ '- Reason: workflow checkpoint and feature commit were already complete, but delayed post-commit activity left the working tree dirty.',
466
+ '',
467
+ '## Recovery guidance',
468
+ '',
469
+ 'The finalized feature commit was kept unchanged for merge. Review these follow-up artifacts separately; do not assume they were merged:',
470
+ '',
471
+ '- `post-completion-status.txt` — original dirty working tree status',
472
+ '- `post-completion-dirty.patch` — unstaged tracked changes',
473
+ '- `post-completion-staged.patch` — staged changes',
474
+ '- `post-completion-untracked.txt` and `untracked/` — untracked files copied before cleanup'
475
+ ) | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-findings.md') -Encoding UTF8
476
+
477
+ & git -C $ProjectRoot reset --hard *> $null
478
+ if ($LASTEXITCODE -ne 0) { return $false }
479
+ foreach ($rel in @($untracked)) {
480
+ if (-not $rel -or $rel -like '.prizmkit/*') { continue }
481
+ $target = Join-Path $ProjectRoot $rel
482
+ if (Test-Path $target) { Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue }
483
+ }
484
+ $remaining = & git -C $ProjectRoot status --porcelain --untracked-files=all 2>$null | Where-Object { $_ -notmatch '^\?\? \.prizmkit/' }
485
+ return [string]::IsNullOrWhiteSpace(($remaining -join "`n"))
486
+ }
487
+
488
+ function Write-PrizmRuntimeFailureLog {
489
+ param(
490
+ [string]$FailureLog,
491
+ [string]$FeatureId,
492
+ [string]$SessionId,
493
+ [string]$SessionStatus,
494
+ [int]$ExitCode,
495
+ [string]$StaleKillMarker,
496
+ [string]$ProgressJson,
497
+ [string]$CheckpointPath,
498
+ [string]$ProjectRoot,
499
+ [string]$BaseCommit
500
+ )
501
+ if (-not $FailureLog -or (Test-Path $FailureLog)) { return }
502
+ $dir = Split-Path $FailureLog -Parent
503
+ if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
504
+ $stale = if (Test-Path $StaleKillMarker) { Get-Content $StaleKillMarker -Raw } else { 'No stale-kill marker.' }
505
+ $progressLines = @('Progress data unavailable.')
506
+ if (Test-Path $ProgressJson) {
507
+ try {
508
+ $progress = Get-Content $ProgressJson -Raw | ConvertFrom-Json
509
+ $progressLines = @()
510
+ foreach ($key in @('fatal_error_code','api_error_status','api_error_code','current_phase','current_tool','last_text_snippet','terminal_result_text')) {
511
+ if ($progress.PSObject.Properties[$key] -and $progress.$key) { $progressLines += "- ${key}: $($progress.$key)" }
512
+ }
513
+ if ($progressLines.Count -eq 0) { $progressLines = @('Progress data contained no terminal fields.') }
514
+ } catch { $progressLines = @("Progress parse error: $($_.Exception.Message)") }
515
+ }
516
+ $checkpointLines = @('No checkpoint file found.')
517
+ if (Test-Path $CheckpointPath) {
518
+ try {
519
+ $checkpoint = Get-Content $CheckpointPath -Raw | ConvertFrom-Json
520
+ $steps = @($checkpoint.steps)
521
+ $complete = @($steps | Where-Object { $_.status -in @('completed','skipped') }).Count
522
+ $checkpointLines = @("$complete/$($steps.Count) steps completed_or_skipped")
523
+ foreach ($step in $steps) {
524
+ if ($step.status -notin @('completed','skipped')) { $checkpointLines += "- incomplete: $($step.id) $($step.skill) = $($step.status)" }
525
+ }
526
+ } catch { $checkpointLines = @("Checkpoint parse error: $($_.Exception.Message)") }
527
+ }
528
+ $latestCommit = (& git -C $ProjectRoot rev-parse --short HEAD 2>$null | Select-Object -First 1)
529
+ if (-not $latestCommit) { $latestCommit = 'unavailable' }
530
+ $featureCommit = if (Get-PrizmFeatureCommit $ProjectRoot $BaseCommit $FeatureId $false) { 'yes' } else { 'no' }
531
+ $dirty = & git -C $ProjectRoot status --short 2>$null
532
+ if ([string]::IsNullOrWhiteSpace(($dirty -join "`n"))) { $dirty = @('clean') }
533
+ @(
534
+ '# Runtime-synthesized failure log',
535
+ '',
536
+ '## Session',
537
+ '',
538
+ "- feature_id: $FeatureId",
539
+ "- session_id: $SessionId",
540
+ "- session_status: $SessionStatus",
541
+ "- exit_code: $ExitCode",
542
+ '',
543
+ '## Stale kill marker',
544
+ '',
545
+ '```json',
546
+ $stale,
547
+ '```',
548
+ '',
549
+ '## Progress',
550
+ '',
551
+ $progressLines,
552
+ '',
553
+ '## Checkpoint',
554
+ '',
555
+ $checkpointLines,
556
+ '',
557
+ '## Git state',
558
+ '',
559
+ "- feature_commit_exists: $featureCommit",
560
+ "- latest_commit: $latestCommit",
561
+ '',
562
+ '```text',
563
+ $dirty,
564
+ '```',
565
+ '',
566
+ '## Recommended recovery action',
567
+ '',
568
+ '- If this is an AI runtime/provider error before checkpoint completion, retry the session with a fresh context.',
569
+ '- If checkpoint completion and a feature commit both exist, inspect post-completion artifacts and finalize manually rather than rebuilding from scratch.',
570
+ '- If the working tree is dirty, preserve or review those changes before any reset or merge.'
571
+ ) | Set-Content -Path $FailureLog -Encoding UTF8
572
+ }
573
+
338
574
  function New-PrizmDefaultDevBranchName {
339
575
  param([string]$Kind, [string]$CurrentItemId)
340
576
  $timestamp = Get-Date -Format 'yyyyMMddHHmm'
@@ -588,6 +824,17 @@ function Invoke-PrizmPipeline {
588
824
  }
589
825
 
590
826
  $effectiveStaleKillThreshold = Get-PrizmEffectiveStaleKillThreshold -ProgressFile $progressJson -BaseThreshold $staleKillThreshold
827
+ $fatalErrorCode = Get-PrizmProgressFatalErrorCode -ProgressFile $progressJson
828
+ if ($fatalErrorCode) {
829
+ $wasStaleKilled = $true
830
+ Write-PrizmWarn "Session hit fatal AI runtime error: $fatalErrorCode"
831
+ $fatalErrorMarker = Join-Path $logsDir 'fatal-error.json'
832
+ Write-PrizmFatalErrorMarker $fatalErrorMarker $fatalErrorCode $staleSeconds $effectiveStaleKillThreshold
833
+ Write-PrizmFatalErrorMarker $staleKillMarker $fatalErrorCode $staleSeconds $effectiveStaleKillThreshold
834
+ Stop-PrizmSessionProcess $pidPath
835
+ if ($staleKillGraceSeconds -gt 0) { Start-Sleep -Seconds $staleKillGraceSeconds }
836
+ break
837
+ }
591
838
  if ($effectiveStaleKillThreshold -gt 0 -and $staleSeconds -ge $effectiveStaleKillThreshold) {
592
839
  $wasStaleKilled = $true
593
840
  Write-PrizmWarn "Session stale-killed (no progress for ${effectiveStaleKillThreshold}s)"
@@ -619,9 +866,23 @@ function Invoke-PrizmPipeline {
619
866
  Stop-PrizmProgressParser $parserProcess
620
867
 
621
868
  $wasInfraError = ($exitCode -ne 0 -and (Test-PrizmInfraError -SessionLog $sessionLog -ProgressJson $progressJson))
869
+ $wasAiRuntimeError = Test-PrizmAiRuntimeError -SessionLog $sessionLog -ProgressJson $progressJson
870
+ $semanticCompletion = if ($Kind -eq 'feature' -and $isGitRepository) {
871
+ Get-PrizmFeatureSemanticCompletion $paths.ProjectRoot $listPath $CurrentItemId $baseCommit $paths.PrizmkitDir
872
+ } else { $null }
622
873
 
623
874
  $status = 'crashed'
624
- if ($wasTimedOut) {
875
+ if ($semanticCompletion) {
876
+ $status = 'success'
877
+ if ($exitCode -ne 0 -or $wasStaleKilled -or $wasTimedOut -or $wasAiRuntimeError) {
878
+ Write-PrizmWarn "Session ended with a failure signal after semantic completion; treating as finalized success"
879
+ Write-PrizmWarn "Semantic completion commit: $($semanticCompletion.CommitSha)"
880
+ }
881
+ } elseif ($wasAiRuntimeError) {
882
+ $status = 'infra_error'
883
+ Write-PrizmWarn "AI session failed due to structured AI runtime/context error"
884
+ Write-PrizmWarn "AI runtime errors are retried without consuming code retry budget"
885
+ } elseif ($wasTimedOut) {
625
886
  $status = 'timed_out'
626
887
  Write-PrizmWarn "AI session timed out after $timeoutSeconds seconds"
627
888
  } elseif ($wasInfraError) {
@@ -653,13 +914,17 @@ function Invoke-PrizmPipeline {
653
914
  $mergeSucceeded = $true
654
915
  $itemListStatus = ''
655
916
  if ($status -eq 'success') {
656
- $updateResult = Invoke-PrizmPythonJson $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'update', $idOption, $CurrentItemId, '--session-id', $sessionId, '--session-status', $status) + $maxRetryArgs)
657
- if ($updateResult -and $updateResult.PSObject.Properties['new_status']) {
658
- $itemListStatus = [string]$updateResult.new_status
659
- }
660
-
661
917
  if (Test-PrizmGitDirty $paths.ProjectRoot) {
662
- if ($hadDirtyBaseline) {
918
+ if ($semanticCompletion) {
919
+ $artifactDir = Join-Path $paths.PrizmkitDir "specs\$($semanticCompletion.Slug)"
920
+ if (Save-PrizmPostCompletionDirtyArtifacts $paths.ProjectRoot $artifactDir $CurrentItemId $sessionId) {
921
+ Write-PrizmWarn "Post-completion dirty changes preserved under $artifactDir"
922
+ Write-PrizmWarn "They were not included in the finalized feature commit."
923
+ } else {
924
+ Write-PrizmWarn "Could not safely preserve post-completion dirty changes; preserving dev branch for manual finalization"
925
+ $status = 'finalization_needed'
926
+ }
927
+ } elseif ($hadDirtyBaseline) {
663
928
  Write-PrizmInfo "Auto-committing pipeline bookkeeping artifacts only."
664
929
  Invoke-PrizmGitIncludeBookkeepingArtifacts $paths.ProjectRoot $stateDir $listPath
665
930
  } else {
@@ -668,13 +933,20 @@ function Invoke-PrizmPipeline {
668
933
  }
669
934
  }
670
935
 
671
- if ($isGitRepository -and $devBranchName) {
672
- if (Merge-PrizmDevBranch $paths.ProjectRoot $devBranchName $originalBranch $autoPush) {
673
- $devBranchName = ''
674
- } else {
675
- $mergeSucceeded = $false
676
- $status = 'merge_conflict'
677
- Write-PrizmWarn "Auto-merge failed - dev branch preserved for inspection"
936
+ if ($status -eq 'success') {
937
+ $updateResult = Invoke-PrizmPythonJson $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'update', $idOption, $CurrentItemId, '--session-id', $sessionId, '--session-status', $status) + $maxRetryArgs)
938
+ if ($updateResult -and $updateResult.PSObject.Properties['new_status']) {
939
+ $itemListStatus = [string]$updateResult.new_status
940
+ }
941
+
942
+ if ($isGitRepository -and $devBranchName) {
943
+ if (Merge-PrizmDevBranch $paths.ProjectRoot $devBranchName $originalBranch $autoPush) {
944
+ $devBranchName = ''
945
+ } else {
946
+ $mergeSucceeded = $false
947
+ $status = 'merge_conflict'
948
+ Write-PrizmWarn "Auto-merge failed - dev branch preserved for inspection"
949
+ }
678
950
  }
679
951
  }
680
952
  } elseif ($isGitRepository -and $devBranchName) {
@@ -685,7 +957,20 @@ function Invoke-PrizmPipeline {
685
957
  Restore-PrizmOriginalBranch $paths.ProjectRoot $originalBranch $devBranchName | Out-Null
686
958
  }
687
959
 
960
+ if ($status -eq 'success' -and $mergeSucceeded -and $isGitRepository) {
961
+ Invoke-PrizmGitCommitPath $paths.ProjectRoot $listPath "chore($CurrentItemId): update $idName status" | Out-Null
962
+ }
963
+
688
964
  if ($status -ne 'success') {
965
+ if ($Kind -eq 'feature') {
966
+ $failureSlug = if ($semanticCompletion) { [string]$semanticCompletion.Slug } else { Get-PrizmFeatureSlugFromList $listPath $CurrentItemId }
967
+ if ($failureSlug) {
968
+ $featureArtifactDir = Join-Path $paths.PrizmkitDir "specs\$failureSlug"
969
+ $failureLog = Join-Path $featureArtifactDir 'failure-log.md'
970
+ $checkpointPath = Join-Path $featureArtifactDir 'workflow-checkpoint.json'
971
+ Write-PrizmRuntimeFailureLog $failureLog $CurrentItemId $sessionId $status $exitCode $staleKillMarker $progressJson $checkpointPath $paths.ProjectRoot $baseCommit
972
+ }
973
+ }
689
974
  $updateResult = Invoke-PrizmPythonJson $python (@((Join-Path $paths.ScriptsDir $updateScript), $listOption, $listPath, '--state-dir', $stateDir, '--action', 'update', $idOption, $CurrentItemId, '--session-id', $sessionId, '--session-status', $status) + $maxRetryArgs)
690
975
  if ($updateResult -and $updateResult.PSObject.Properties['new_status']) {
691
976
  $itemListStatus = [string]$updateResult.new_status