prizmkit 1.1.70 → 1.1.74

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.
Files changed (57) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/agents/prizm-dev-team-dev.md +11 -1
  3. package/bundled/dev-pipeline/lib/common.sh +427 -0
  4. package/bundled/dev-pipeline/lib/heartbeat.sh +101 -36
  5. package/bundled/dev-pipeline/run-feature.sh +109 -29
  6. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +198 -3
  7. package/bundled/dev-pipeline/scripts/update-feature-status.py +27 -3
  8. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +21 -0
  9. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +1 -1
  10. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +5 -9
  11. package/bundled/dev-pipeline/templates/sections/feature-context.md +3 -18
  12. package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +11 -0
  13. package/bundled/dev-pipeline/templates/sections/phase-commit.md +11 -0
  14. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +1 -1
  15. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +6 -12
  16. package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +10 -3
  17. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
  18. package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +4 -8
  19. package/bundled/dev-pipeline-windows/lib/common.ps1 +61 -1
  20. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +325 -16
  21. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +198 -3
  22. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +27 -3
  23. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +21 -0
  24. package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +1 -1
  25. package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +27 -0
  26. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +543 -14
  27. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +664 -14
  28. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +741 -14
  29. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +2 -2
  30. package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +1 -1
  31. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +1 -1
  32. package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +1 -1
  33. package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +3 -3
  34. package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +1 -1
  35. package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +3 -18
  36. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +239 -40
  37. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +75 -26
  38. package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +142 -36
  39. package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +13 -2
  40. package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +12 -1
  41. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +1 -1
  42. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +7 -17
  43. package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +10 -3
  44. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +1 -1
  45. package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +1 -1
  46. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +3 -1
  47. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +7 -3
  48. package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +1 -3
  49. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +1 -1
  50. package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +1 -1
  51. package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +1 -1
  52. package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +2 -2
  53. package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +13 -17
  54. package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +2 -4
  55. package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +1 -1
  56. package/bundled/skills/_metadata.json +1 -1
  57. package/package.json +1 -1
@@ -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'
@@ -575,7 +811,32 @@ function Invoke-PrizmPipeline {
575
811
  $childAdvanced = ($childSignature -and $childSignature -ne $previousChildActivitySignature)
576
812
  $previousChildActivitySignature = $childSignature
577
813
 
578
- if ($growth -gt 0 -or $childAdvanced) {
814
+ $effectiveStaleKillThreshold = Get-PrizmEffectiveStaleKillThreshold -ProgressFile $progressJson -BaseThreshold $staleKillThreshold
815
+
816
+ # Check for error-loop: agent is actively producing output but results are
817
+ # all read-offset errors or wasted calls.
818
+ $errorLoopDetected = $false
819
+ if ($effectiveStaleKillThreshold -gt 0 -and $growth -gt 0 -and (Test-Path $progressJson)) {
820
+ try {
821
+ $progress = Get-Content $progressJson -Raw | ConvertFrom-Json
822
+ $errors = $progress.errors
823
+ if ($errors -is [array] -and $errors.Count -ge 5) {
824
+ $recent = $errors[-5..-1]
825
+ $allBad = ($recent | Where-Object {
826
+ $_.type -in @("read_offset_overflow", "wasted_call")
827
+ }).Count -eq 5
828
+ if ($allBad) {
829
+ $errorLoopDetected = $true
830
+ }
831
+ }
832
+ } catch {
833
+ # Ignore JSON parse errors — progress file may be incomplete or malformed
834
+ }
835
+ }
836
+
837
+ if ($errorLoopDetected) {
838
+ $staleSeconds = $effectiveStaleKillThreshold
839
+ } elseif ($growth -gt 0 -or $childAdvanced) {
579
840
  $staleSeconds = 0
580
841
  } else {
581
842
  $staleSeconds += $waitSeconds
@@ -587,7 +848,17 @@ function Invoke-PrizmPipeline {
587
848
  break
588
849
  }
589
850
 
590
- $effectiveStaleKillThreshold = Get-PrizmEffectiveStaleKillThreshold -ProgressFile $progressJson -BaseThreshold $staleKillThreshold
851
+ $fatalErrorCode = Get-PrizmProgressFatalErrorCode -ProgressFile $progressJson
852
+ if ($fatalErrorCode) {
853
+ $wasStaleKilled = $true
854
+ Write-PrizmWarn "Session hit fatal AI runtime error: $fatalErrorCode"
855
+ $fatalErrorMarker = Join-Path $logsDir 'fatal-error.json'
856
+ Write-PrizmFatalErrorMarker $fatalErrorMarker $fatalErrorCode $staleSeconds $effectiveStaleKillThreshold
857
+ Write-PrizmFatalErrorMarker $staleKillMarker $fatalErrorCode $staleSeconds $effectiveStaleKillThreshold
858
+ Stop-PrizmSessionProcess $pidPath
859
+ if ($staleKillGraceSeconds -gt 0) { Start-Sleep -Seconds $staleKillGraceSeconds }
860
+ break
861
+ }
591
862
  if ($effectiveStaleKillThreshold -gt 0 -and $staleSeconds -ge $effectiveStaleKillThreshold) {
592
863
  $wasStaleKilled = $true
593
864
  Write-PrizmWarn "Session stale-killed (no progress for ${effectiveStaleKillThreshold}s)"
@@ -619,9 +890,23 @@ function Invoke-PrizmPipeline {
619
890
  Stop-PrizmProgressParser $parserProcess
620
891
 
621
892
  $wasInfraError = ($exitCode -ne 0 -and (Test-PrizmInfraError -SessionLog $sessionLog -ProgressJson $progressJson))
893
+ $wasAiRuntimeError = Test-PrizmAiRuntimeError -SessionLog $sessionLog -ProgressJson $progressJson
894
+ $semanticCompletion = if ($Kind -eq 'feature' -and $isGitRepository) {
895
+ Get-PrizmFeatureSemanticCompletion $paths.ProjectRoot $listPath $CurrentItemId $baseCommit $paths.PrizmkitDir
896
+ } else { $null }
622
897
 
623
898
  $status = 'crashed'
624
- if ($wasTimedOut) {
899
+ if ($semanticCompletion) {
900
+ $status = 'success'
901
+ if ($exitCode -ne 0 -or $wasStaleKilled -or $wasTimedOut -or $wasAiRuntimeError) {
902
+ Write-PrizmWarn "Session ended with a failure signal after semantic completion; treating as finalized success"
903
+ Write-PrizmWarn "Semantic completion commit: $($semanticCompletion.CommitSha)"
904
+ }
905
+ } elseif ($wasAiRuntimeError) {
906
+ $status = 'infra_error'
907
+ Write-PrizmWarn "AI session failed due to structured AI runtime/context error"
908
+ Write-PrizmWarn "AI runtime errors are retried without consuming code retry budget"
909
+ } elseif ($wasTimedOut) {
625
910
  $status = 'timed_out'
626
911
  Write-PrizmWarn "AI session timed out after $timeoutSeconds seconds"
627
912
  } elseif ($wasInfraError) {
@@ -653,13 +938,17 @@ function Invoke-PrizmPipeline {
653
938
  $mergeSucceeded = $true
654
939
  $itemListStatus = ''
655
940
  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
941
  if (Test-PrizmGitDirty $paths.ProjectRoot) {
662
- if ($hadDirtyBaseline) {
942
+ if ($semanticCompletion) {
943
+ $artifactDir = Join-Path $paths.PrizmkitDir "specs\$($semanticCompletion.Slug)"
944
+ if (Save-PrizmPostCompletionDirtyArtifacts $paths.ProjectRoot $artifactDir $CurrentItemId $sessionId) {
945
+ Write-PrizmWarn "Post-completion dirty changes preserved under $artifactDir"
946
+ Write-PrizmWarn "They were not included in the finalized feature commit."
947
+ } else {
948
+ Write-PrizmWarn "Could not safely preserve post-completion dirty changes; preserving dev branch for manual finalization"
949
+ $status = 'finalization_needed'
950
+ }
951
+ } elseif ($hadDirtyBaseline) {
663
952
  Write-PrizmInfo "Auto-committing pipeline bookkeeping artifacts only."
664
953
  Invoke-PrizmGitIncludeBookkeepingArtifacts $paths.ProjectRoot $stateDir $listPath
665
954
  } else {
@@ -668,13 +957,20 @@ function Invoke-PrizmPipeline {
668
957
  }
669
958
  }
670
959
 
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"
960
+ if ($status -eq 'success') {
961
+ $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)
962
+ if ($updateResult -and $updateResult.PSObject.Properties['new_status']) {
963
+ $itemListStatus = [string]$updateResult.new_status
964
+ }
965
+
966
+ if ($isGitRepository -and $devBranchName) {
967
+ if (Merge-PrizmDevBranch $paths.ProjectRoot $devBranchName $originalBranch $autoPush) {
968
+ $devBranchName = ''
969
+ } else {
970
+ $mergeSucceeded = $false
971
+ $status = 'merge_conflict'
972
+ Write-PrizmWarn "Auto-merge failed - dev branch preserved for inspection"
973
+ }
678
974
  }
679
975
  }
680
976
  } elseif ($isGitRepository -and $devBranchName) {
@@ -685,7 +981,20 @@ function Invoke-PrizmPipeline {
685
981
  Restore-PrizmOriginalBranch $paths.ProjectRoot $originalBranch $devBranchName | Out-Null
686
982
  }
687
983
 
984
+ if ($status -eq 'success' -and $mergeSucceeded -and $isGitRepository) {
985
+ Invoke-PrizmGitCommitPath $paths.ProjectRoot $listPath "chore($CurrentItemId): update $idName status" | Out-Null
986
+ }
987
+
688
988
  if ($status -ne 'success') {
989
+ if ($Kind -eq 'feature') {
990
+ $failureSlug = if ($semanticCompletion) { [string]$semanticCompletion.Slug } else { Get-PrizmFeatureSlugFromList $listPath $CurrentItemId }
991
+ if ($failureSlug) {
992
+ $featureArtifactDir = Join-Path $paths.PrizmkitDir "specs\$failureSlug"
993
+ $failureLog = Join-Path $featureArtifactDir 'failure-log.md'
994
+ $checkpointPath = Join-Path $featureArtifactDir 'workflow-checkpoint.json'
995
+ Write-PrizmRuntimeFailureLog $failureLog $CurrentItemId $sessionId $status $exitCode $staleKillMarker $progressJson $checkpointPath $paths.ProjectRoot $baseCommit
996
+ }
997
+ }
689
998
  $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
999
  if ($updateResult -and $updateResult.PSObject.Properties['new_status']) {
691
1000
  $itemListStatus = [string]$updateResult.new_status