prizmkit 1.1.89 → 1.1.91

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "frameworkVersion": "1.1.89",
3
- "bundledAt": "2026-07-01T15:42:06.738Z",
4
- "bundledFrom": "4e9108f"
2
+ "frameworkVersion": "1.1.91",
3
+ "bundledAt": "2026-07-01T17:51:47.362Z",
4
+ "bundledFrom": "5dcb047"
5
5
  }
@@ -713,107 +713,6 @@ PY
713
713
  return 0
714
714
  }
715
715
 
716
- prizm_preserve_post_completion_dirty() {
717
- local project_root="$1"
718
- local artifact_dir="$2"
719
- local feature_id="$3"
720
- local session_id="${4:-}"
721
-
722
- git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
723
-
724
- local dirty_status
725
- dirty_status=$(git -C "$project_root" status --porcelain --untracked-files=all 2>/dev/null || true)
726
- [[ -n "$dirty_status" ]] || return 0
727
-
728
- mkdir -p "$artifact_dir" || return 1
729
-
730
- local status_file="$artifact_dir/post-completion-status.txt"
731
- local patch_file="$artifact_dir/post-completion-dirty.patch"
732
- local staged_patch_file="$artifact_dir/post-completion-staged.patch"
733
- local untracked_manifest="$artifact_dir/post-completion-untracked.txt"
734
- local findings_file="$artifact_dir/post-completion-findings.md"
735
- local untracked_dir="$artifact_dir/untracked"
736
-
737
- printf '%s\n' "$dirty_status" > "$status_file" || return 1
738
- git -C "$project_root" diff --binary > "$patch_file" 2>/dev/null || return 1
739
- git -C "$project_root" diff --cached --binary > "$staged_patch_file" 2>/dev/null || return 1
740
-
741
- : > "$untracked_manifest" || return 1
742
- local untracked_tmp
743
- untracked_tmp=$(mktemp 2>/dev/null || mktemp -t prizm-untracked) || return 1
744
- git -C "$project_root" ls-files --others --exclude-standard -z > "$untracked_tmp" 2>/dev/null || {
745
- rm -f "$untracked_tmp"
746
- return 1
747
- }
748
-
749
- if [[ -s "$untracked_tmp" ]]; then
750
- mkdir -p "$untracked_dir" || {
751
- rm -f "$untracked_tmp"
752
- return 1
753
- }
754
- while IFS= read -r -d '' rel_path; do
755
- [[ -n "$rel_path" ]] || continue
756
- printf '%s\n' "$rel_path" >> "$untracked_manifest" || {
757
- rm -f "$untracked_tmp"
758
- return 1
759
- }
760
- local source_path="$project_root/$rel_path"
761
- local dest_path="$untracked_dir/$rel_path"
762
- mkdir -p "$(dirname "$dest_path")" || {
763
- rm -f "$untracked_tmp"
764
- return 1
765
- }
766
- if [[ -f "$source_path" ]]; then
767
- cp -p "$source_path" "$dest_path" || {
768
- rm -f "$untracked_tmp"
769
- return 1
770
- }
771
- elif [[ -d "$source_path" ]]; then
772
- mkdir -p "$dest_path" || {
773
- rm -f "$untracked_tmp"
774
- return 1
775
- }
776
- fi
777
- done < "$untracked_tmp"
778
- fi
779
-
780
- cat > "$findings_file" <<EOF
781
- # Post-completion dirty changes preserved
782
-
783
- - Feature: $feature_id
784
- - Session: ${session_id:-unknown}
785
- - Reason: workflow checkpoint and feature commit were already complete, but delayed post-commit activity left the working tree dirty.
786
-
787
- ## Recovery guidance
788
-
789
- The finalized feature commit was kept unchanged for merge. Review these follow-up artifacts separately; do not assume they were merged:
790
-
791
- - \`post-completion-status.txt\` — original dirty working tree status
792
- - \`post-completion-dirty.patch\` — unstaged tracked changes
793
- - \`post-completion-staged.patch\` — staged changes
794
- - \`post-completion-untracked.txt\` and \`untracked/\` — untracked files copied before cleanup
795
- EOF
796
-
797
- git -C "$project_root" reset --hard >/dev/null 2>&1 || {
798
- rm -f "$untracked_tmp"
799
- return 1
800
- }
801
-
802
- while IFS= read -r -d '' rel_path; do
803
- [[ -n "$rel_path" ]] || continue
804
- case "$rel_path" in
805
- .prizmkit/*) continue ;;
806
- esac
807
- rm -f "$project_root/$rel_path" 2>/dev/null || true
808
- done < "$untracked_tmp"
809
- rm -f "$untracked_tmp"
810
-
811
- dirty_status=$(git -C "$project_root" status --porcelain --untracked-files=all 2>/dev/null | grep -v '^?? .prizmkit/' || true)
812
- [[ -z "$dirty_status" ]] || return 1
813
-
814
- return 0
815
- }
816
-
817
716
  prizm_synthesize_failure_log() {
818
717
  local failure_log="$1"
819
718
  local feature_id="$2"
@@ -934,7 +833,6 @@ $dirty_summary
934
833
  ## Recommended recovery action
935
834
 
936
835
  - If this is an AI runtime/provider error before checkpoint completion, retry the session with a fresh context.
937
- - If checkpoint completion and a feature commit both exist, inspect post-completion artifacts and finalize manually rather than rebuilding from scratch.
938
836
  - If the working tree is dirty, preserve or review those changes before any reset or merge.
939
837
  EOF
940
838
  }
@@ -43,6 +43,8 @@ start_heartbeat() {
43
43
  local prev_size=0
44
44
  local prev_max_size=0
45
45
  local prev_child_activity_signature=""
46
+ local prev_progress_signature=""
47
+ local prev_error_loop_signature=""
46
48
  local stale_seconds=0
47
49
  while kill -0 "$cli_pid" 2>/dev/null; do
48
50
  sleep "$heartbeat_interval"
@@ -77,9 +79,10 @@ start_heartbeat() {
77
79
  local child_activity_signature=""
78
80
  local child_total_bytes=0
79
81
  local child_session_count=0
82
+ local progress_signature=""
80
83
  if [[ -f "$progress_json" ]]; then
81
- local child_activity_data
82
- child_activity_data=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
84
+ local progress_activity_data
85
+ progress_activity_data=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
83
86
  import json
84
87
  import sys
85
88
 
@@ -89,14 +92,31 @@ try:
89
92
  except Exception:
90
93
  sys.exit(0)
91
94
 
92
- signature = str(progress.get("child_activity_signature") or "")
95
+ child_signature = str(progress.get("child_activity_signature") or "")
93
96
  total_bytes = int(progress.get("child_total_bytes") or 0)
94
97
  session_count = len(progress.get("child_session_files") or [])
95
- print(f"{signature}\t{total_bytes}\t{session_count}")
98
+ progress_signature = json.dumps(
99
+ [
100
+ progress.get("current_phase") or "",
101
+ progress.get("current_tool") or "",
102
+ int(progress.get("message_count") or 0),
103
+ int(progress.get("total_tool_calls") or 0),
104
+ int(progress.get("active_subagent_count") or 0),
105
+ int(progress.get("subagent_spawn_count") or 0),
106
+ child_signature,
107
+ total_bytes,
108
+ session_count,
109
+ progress.get("fatal_error_code") or "",
110
+ progress.get("terminal_result_text") or "",
111
+ ],
112
+ separators=(",", ":"),
113
+ )
114
+ sep = "\x1f"
115
+ print(sep.join([child_signature, str(total_bytes), str(session_count), progress_signature]))
96
116
  PY
97
117
  )
98
- if [[ -n "$child_activity_data" ]]; then
99
- IFS=$'\t' read -r child_activity_signature child_total_bytes child_session_count <<< "$child_activity_data"
118
+ if [[ -n "$progress_activity_data" ]]; then
119
+ IFS=$'\037' read -r child_activity_signature child_total_bytes child_session_count progress_signature <<< "$progress_activity_data"
100
120
  fi
101
121
  fi
102
122
 
@@ -106,6 +126,12 @@ PY
106
126
  fi
107
127
  prev_child_activity_signature="$child_activity_signature"
108
128
 
129
+ local progress_advanced=0
130
+ if [[ -n "$progress_signature" && "$progress_signature" != "$prev_progress_signature" ]]; then
131
+ progress_advanced=1
132
+ fi
133
+ prev_progress_signature="$progress_signature"
134
+
109
135
  local effective_stale_kill_threshold="$stale_kill_threshold"
110
136
  if [[ $stale_kill_threshold -gt 0 && -f "$progress_json" ]]; then
111
137
  local extended_threshold
@@ -172,8 +198,11 @@ PY
172
198
 
173
199
  # Check for error-loop: agent is actively producing output but results are
174
200
  # all read-offset errors or wasted calls. This is a stuck agent that appears
175
- # "active" by log growth but is accomplishing nothing.
201
+ # "active" by log growth but is accomplishing nothing. Only a newly advancing
202
+ # error signature counts; stale historical errors must not turn later normal
203
+ # output into a stale kill.
176
204
  local error_loop_detected=false
205
+ local error_loop_signature=""
177
206
  if [[ $effective_stale_kill_threshold -gt 0 && $growth -gt 0 && -f "$progress_json" ]]; then
178
207
  local error_loop_flag
179
208
  error_loop_flag=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
@@ -187,33 +216,38 @@ errors = progress.get("errors", [])
187
216
  if isinstance(errors, list) and len(errors) >= 5:
188
217
  recent = errors[-5:]
189
218
  if all(isinstance(e, dict) and e.get("type") in ("read_offset_overflow", "wasted_call") for e in recent):
190
- print("error_loop")
219
+ print(json.dumps(recent, sort_keys=True, separators=(",", ":")))
191
220
  PY
192
221
  )
193
- if [[ "$error_loop_flag" == "error_loop" ]]; then
194
- error_loop_detected=true
222
+ if [[ -n "$error_loop_flag" ]]; then
223
+ error_loop_signature="$error_loop_flag"
224
+ if [[ -n "$prev_error_loop_signature" && "$error_loop_signature" != "$prev_error_loop_signature" ]]; then
225
+ error_loop_detected=true
226
+ fi
195
227
  fi
196
228
  fi
229
+ prev_error_loop_signature="$error_loop_signature"
197
230
 
198
231
  # Track progress staleness. Parent sessions can sit in a wait/polling
199
- # tool while child transcripts keep growing, so child activity counts.
200
- # Error loops bypass normal growth-as-progress because the log is only
201
- # growing with repeated failed reads or wasted calls.
232
+ # tool while child transcripts or structured progress counters keep
233
+ # advancing, so both child activity and progress.json state changes
234
+ # count as progress. Error loops bypass normal growth-as-progress
235
+ # because the log is only growing with repeated failed reads or wasted
236
+ # calls.
202
237
  #
203
238
  # When the main session log has been truncated (e.g. by subagent stdout
204
- # redirection severing the file descriptor), use child activity as the
205
- # primary progress signal — the parent process is still running and children
206
- # are still producing output, so this is not a true stall.
239
+ # redirection severing the file descriptor), use structured progress and
240
+ # child activity as the primary progress signals — the parent process is
241
+ # still running and children may still be producing output.
207
242
  if [[ "$error_loop_detected" == "true" ]]; then
208
243
  stale_seconds=$effective_stale_kill_threshold
209
244
  elif [[ "$log_truncated" == "true" ]]; then
210
- # Log truncated: rely on child growth and CLI process liveness.
211
- # Don't increment staleness — the process is still working.
212
- if [[ $child_growth -gt 0 ]]; then
245
+ if [[ $progress_advanced -gt 0 || $child_growth -gt 0 ]]; then
213
246
  stale_seconds=0
247
+ else
248
+ stale_seconds=$((stale_seconds + heartbeat_interval))
214
249
  fi
215
- # If neither log nor children grow, this is a real stall
216
- elif [[ $growth -le 0 && $child_growth -eq 0 ]]; then
250
+ elif [[ $growth -le 0 && $child_growth -eq 0 && $progress_advanced -eq 0 ]]; then
217
251
  stale_seconds=$((stale_seconds + heartbeat_interval))
218
252
  else
219
253
  stale_seconds=0
@@ -251,7 +285,7 @@ PY
251
285
  local status_icon
252
286
  if $log_truncated; then
253
287
  status_icon="${RED}⚠${NC}"
254
- elif [[ $growth -gt 0 || $child_growth -gt 0 ]]; then
288
+ elif [[ $growth -gt 0 || $child_growth -gt 0 || $progress_advanced -gt 0 ]]; then
255
289
  status_icon="${GREEN}▶${NC}"
256
290
  else
257
291
  status_icon="${YELLOW}⏸${NC}"
@@ -264,28 +264,11 @@ spawn_and_wait_session() {
264
264
  local dirty_files=""
265
265
  dirty_files=$(git -C "$project_root" status --porcelain 2>/dev/null || true)
266
266
  if [[ -n "$dirty_files" ]]; then
267
- if [[ "$semantic_finalized" == true ]]; then
268
- local post_completion_slug="$semantic_feature_slug"
269
- if [[ -z "$post_completion_slug" ]]; then
270
- post_completion_slug=$(prizm_feature_slug_from_list "$feature_list" "$feature_id" 2>/dev/null || true)
271
- fi
272
- if [[ -n "$post_completion_slug" ]] && prizm_preserve_post_completion_dirty "$project_root" "$PRIZMKIT_DIR/specs/${post_completion_slug}" "$feature_id" "$session_id"; then
273
- log_warn "Post-completion dirty changes preserved under $PRIZMKIT_DIR/specs/${post_completion_slug}/"
274
- log_warn "They were not included in the finalized feature commit."
275
- else
276
- log_warn "Could not safely preserve post-completion dirty changes; preserving dev branch for manual finalization"
277
- session_status="finalization_needed"
278
- fi
279
- else
280
- # Auto-commit any remaining dirty files produced during a normal
281
- # clean success path. Semantic finalization explicitly avoids this
282
- # so delayed post-commit findings cannot be merged into main.
283
- log_info "Auto-committing remaining session artifacts..."
284
- git -C "$project_root" add -A 2>/dev/null || true
285
- git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
286
- || git -C "$project_root" commit --no-verify -m "chore($feature_id): include remaining session artifacts" 2>/dev/null \
287
- || true
288
- fi
267
+ log_info "Auto-committing remaining session artifacts..."
268
+ git -C "$project_root" add -A 2>/dev/null || true
269
+ git -C "$project_root" commit --no-verify --amend --no-edit 2>/dev/null \
270
+ || git -C "$project_root" commit --no-verify -m "chore($feature_id): include remaining session artifacts" 2>/dev/null \
271
+ || true
289
272
  fi
290
273
  fi
291
274
  fi
@@ -49,7 +49,6 @@ SESSION_STATUS_VALUES = [
49
49
  "commit_missing",
50
50
  "docs_missing",
51
51
  "merge_conflict",
52
- "finalization_needed",
53
52
  ]
54
53
 
55
54
  TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped", "split"}
@@ -649,22 +648,6 @@ def action_update(args, feature_list_path, state_dir):
649
648
  fs["last_session_id"] = session_id
650
649
  fs["last_failed_session_id"] = session_id
651
650
 
652
- err = update_feature_in_list(feature_list_path, feature_id, new_status)
653
- if err:
654
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
655
- return
656
- elif session_status == "finalization_needed":
657
- # Runtime preserved dirty post-completion changes but could not safely
658
- # clean them for automatic merge. Preserve the dev branch and stop for
659
- # manual finalization instead of spending code retry budget.
660
- new_status = "failed"
661
- fs["degraded_reason"] = session_status
662
- fs["resume_from_phase"] = None
663
- fs["finalization_needed_count"] = fs.get("finalization_needed_count", 0) + 1
664
- if session_id:
665
- fs["last_session_id"] = session_id
666
- fs["last_failed_session_id"] = session_id
667
-
668
651
  err = update_feature_in_list(feature_list_path, feature_id, new_status)
669
652
  if err:
670
653
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
@@ -736,9 +719,9 @@ def action_update(args, feature_list_path, state_dir):
736
719
  }
737
720
  if auto_skipped_features:
738
721
  summary["auto_skipped"] = [info["feature_id"] for info in auto_skipped_features]
739
- if session_status in ("commit_missing", "docs_missing", "merge_conflict", "finalization_needed"):
722
+ if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
740
723
  summary["degraded_reason"] = session_status
741
- summary["restart_policy"] = "manual_finalization" if session_status == "finalization_needed" else "finalization_retry"
724
+ summary["restart_policy"] = "finalization_retry"
742
725
  elif session_status == "infra_error":
743
726
  summary["restart_policy"] = "infra_retry"
744
727
  summary["infra_error_count"] = fs.get("infra_error_count", 0)
@@ -467,13 +467,14 @@ function Get-PrizmEffectiveStaleKillThreshold {
467
467
  return [Math]::Max($BaseThreshold * 4, 3600)
468
468
  }
469
469
 
470
- function Get-PrizmProgressChildActivity {
470
+ function Get-PrizmProgressActivity {
471
471
  param([string]$ProgressFile)
472
472
 
473
473
  $empty = [pscustomobject]@{
474
- Signature = ''
475
- TotalBytes = 0
476
- SessionCount = 0
474
+ ChildSignature = ''
475
+ ChildTotalBytes = 0
476
+ ChildSessionCount = 0
477
+ ProgressSignature = ''
477
478
  }
478
479
  if (-not (Test-Path $ProgressFile)) { return $empty }
479
480
 
@@ -483,25 +484,72 @@ function Get-PrizmProgressChildActivity {
483
484
  return $empty
484
485
  }
485
486
 
486
- $signature = ''
487
+ $childSignature = ''
487
488
  if ($progress.PSObject.Properties['child_activity_signature'] -and $progress.child_activity_signature) {
488
- $signature = [string]$progress.child_activity_signature
489
+ $childSignature = [string]$progress.child_activity_signature
489
490
  }
490
491
 
491
- $totalBytes = [int64]0
492
+ $childTotalBytes = [int64]0
492
493
  if ($progress.PSObject.Properties['child_total_bytes']) {
493
- [int64]::TryParse([string]$progress.child_total_bytes, [ref]$totalBytes) | Out-Null
494
+ [int64]::TryParse([string]$progress.child_total_bytes, [ref]$childTotalBytes) | Out-Null
494
495
  }
495
496
 
496
- $sessionCount = 0
497
+ $childSessionCount = 0
497
498
  if ($progress.PSObject.Properties['child_session_files'] -and $progress.child_session_files) {
498
- $sessionCount = @($progress.child_session_files).Count
499
+ $childSessionCount = @($progress.child_session_files).Count
500
+ }
501
+
502
+ $messageCount = 0
503
+ if ($progress.PSObject.Properties['message_count']) {
504
+ [int]::TryParse([string]$progress.message_count, [ref]$messageCount) | Out-Null
505
+ }
506
+ $totalToolCalls = 0
507
+ if ($progress.PSObject.Properties['total_tool_calls']) {
508
+ [int]::TryParse([string]$progress.total_tool_calls, [ref]$totalToolCalls) | Out-Null
509
+ }
510
+ $activeSubagentCount = 0
511
+ if ($progress.PSObject.Properties['active_subagent_count']) {
512
+ [int]::TryParse([string]$progress.active_subagent_count, [ref]$activeSubagentCount) | Out-Null
513
+ }
514
+ $subagentSpawnCount = 0
515
+ if ($progress.PSObject.Properties['subagent_spawn_count']) {
516
+ [int]::TryParse([string]$progress.subagent_spawn_count, [ref]$subagentSpawnCount) | Out-Null
517
+ }
518
+ $currentPhase = if ($progress.PSObject.Properties['current_phase'] -and $progress.current_phase) { [string]$progress.current_phase } else { '' }
519
+ $currentTool = if ($progress.PSObject.Properties['current_tool'] -and $progress.current_tool) { [string]$progress.current_tool } else { '' }
520
+ $fatalErrorCode = if ($progress.PSObject.Properties['fatal_error_code'] -and $progress.fatal_error_code) { [string]$progress.fatal_error_code } else { '' }
521
+ $terminalResultText = if ($progress.PSObject.Properties['terminal_result_text'] -and $progress.terminal_result_text) { [string]$progress.terminal_result_text } else { '' }
522
+
523
+ $progressSignature = @(
524
+ $currentPhase,
525
+ $currentTool,
526
+ $messageCount,
527
+ $totalToolCalls,
528
+ $activeSubagentCount,
529
+ $subagentSpawnCount,
530
+ $childSignature,
531
+ $childTotalBytes,
532
+ $childSessionCount,
533
+ $fatalErrorCode,
534
+ $terminalResultText
535
+ ) | ConvertTo-Json -Compress
536
+
537
+ return [pscustomobject]@{
538
+ ChildSignature = $childSignature
539
+ ChildTotalBytes = $childTotalBytes
540
+ ChildSessionCount = $childSessionCount
541
+ ProgressSignature = $progressSignature
499
542
  }
543
+ }
544
+
545
+ function Get-PrizmProgressChildActivity {
546
+ param([string]$ProgressFile)
500
547
 
548
+ $activity = Get-PrizmProgressActivity -ProgressFile $ProgressFile
501
549
  return [pscustomobject]@{
502
- Signature = $signature
503
- TotalBytes = $totalBytes
504
- SessionCount = $sessionCount
550
+ Signature = $activity.ChildSignature
551
+ TotalBytes = $activity.ChildTotalBytes
552
+ SessionCount = $activity.ChildSessionCount
505
553
  }
506
554
  }
507
555
 
@@ -438,60 +438,6 @@ function Invoke-PrizmPipeline {
438
438
  return [pscustomobject]@{ Slug = $slug; CommitSha = $commitSha; CheckpointPath = $checkpointPath }
439
439
  }
440
440
 
441
- function Save-PrizmPostCompletionDirtyArtifacts {
442
- param([string]$ProjectRoot, [string]$ArtifactDir, [string]$ItemId, [string]$SessionId)
443
- $status = & git -C $ProjectRoot status --porcelain --untracked-files=all 2>$null
444
- if ([string]::IsNullOrWhiteSpace(($status -join "`n"))) { return $true }
445
-
446
- New-Item -ItemType Directory -Force -Path $ArtifactDir | Out-Null
447
- ($status -join "`n") | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-status.txt') -Encoding UTF8
448
- & git -C $ProjectRoot diff --binary | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-dirty.patch') -Encoding UTF8
449
- if ($LASTEXITCODE -ne 0) { return $false }
450
- & git -C $ProjectRoot diff --cached --binary | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-staged.patch') -Encoding UTF8
451
- if ($LASTEXITCODE -ne 0) { return $false }
452
-
453
- $untracked = & git -C $ProjectRoot ls-files --others --exclude-standard 2>$null
454
- $manifest = Join-Path $ArtifactDir 'post-completion-untracked.txt'
455
- @($untracked) | Set-Content -Path $manifest -Encoding UTF8
456
- $untrackedDir = Join-Path $ArtifactDir 'untracked'
457
- foreach ($rel in @($untracked)) {
458
- if (-not $rel) { continue }
459
- $source = Join-Path $ProjectRoot $rel
460
- $dest = Join-Path $untrackedDir $rel
461
- $destParent = Split-Path $dest -Parent
462
- if ($destParent) { New-Item -ItemType Directory -Force -Path $destParent | Out-Null }
463
- if (Test-Path $source -PathType Leaf) { Copy-Item -LiteralPath $source -Destination $dest -Force }
464
- elseif (Test-Path $source -PathType Container) { New-Item -ItemType Directory -Force -Path $dest | Out-Null }
465
- }
466
-
467
- @(
468
- '# Post-completion dirty changes preserved',
469
- '',
470
- "- Feature: $ItemId",
471
- "- Session: $SessionId",
472
- '- Reason: workflow checkpoint and feature commit were already complete, but delayed post-commit activity left the working tree dirty.',
473
- '',
474
- '## Recovery guidance',
475
- '',
476
- 'The finalized feature commit was kept unchanged for merge. Review these follow-up artifacts separately; do not assume they were merged:',
477
- '',
478
- '- `post-completion-status.txt` — original dirty working tree status',
479
- '- `post-completion-dirty.patch` — unstaged tracked changes',
480
- '- `post-completion-staged.patch` — staged changes',
481
- '- `post-completion-untracked.txt` and `untracked/` — untracked files copied before cleanup'
482
- ) | Set-Content -Path (Join-Path $ArtifactDir 'post-completion-findings.md') -Encoding UTF8
483
-
484
- & git -C $ProjectRoot reset --hard *> $null
485
- if ($LASTEXITCODE -ne 0) { return $false }
486
- foreach ($rel in @($untracked)) {
487
- if (-not $rel -or $rel -like '.prizmkit/*') { continue }
488
- $target = Join-Path $ProjectRoot $rel
489
- if (Test-Path $target) { Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue }
490
- }
491
- $remaining = & git -C $ProjectRoot status --porcelain --untracked-files=all 2>$null | Where-Object { $_ -notmatch '^\?\? \.prizmkit/' }
492
- return [string]::IsNullOrWhiteSpace(($remaining -join "`n"))
493
- }
494
-
495
441
  function Write-PrizmRuntimeFailureLog {
496
442
  param(
497
443
  [string]$FailureLog,
@@ -573,7 +519,6 @@ function Invoke-PrizmPipeline {
573
519
  '## Recommended recovery action',
574
520
  '',
575
521
  '- If this is an AI runtime/provider error before checkpoint completion, retry the session with a fresh context.',
576
- '- If checkpoint completion and a feature commit both exist, inspect post-completion artifacts and finalize manually rather than rebuilding from scratch.',
577
522
  '- If the working tree is dirty, preserve or review those changes before any reset or merge.'
578
523
  ) | Set-Content -Path $FailureLog -Encoding UTF8
579
524
  }
@@ -795,7 +740,9 @@ function Invoke-PrizmPipeline {
795
740
  $elapsedSeconds = 0
796
741
  $staleSeconds = 0
797
742
  $previousLogSize = 0
743
+ $previousProgressSignature = ''
798
744
  $previousChildActivitySignature = ''
745
+ $previousErrorLoopSignature = ''
799
746
  $wasTimedOut = $false
800
747
  $staleKillMarker = Join-Path $logsDir 'stale-kill.json'
801
748
  $wasStaleKilled = $false
@@ -813,16 +760,22 @@ function Invoke-PrizmPipeline {
813
760
  $growth = $currentLogSize - $previousLogSize
814
761
  $previousLogSize = $currentLogSize
815
762
 
816
- $childActivity = Get-PrizmProgressChildActivity -ProgressFile $progressJson
817
- $childSignature = [string]$childActivity.Signature
763
+ $progressActivity = Get-PrizmProgressActivity -ProgressFile $progressJson
764
+ $progressSignature = [string]$progressActivity.ProgressSignature
765
+ $progressAdvanced = ($progressSignature -and $progressSignature -ne $previousProgressSignature)
766
+ $previousProgressSignature = $progressSignature
767
+ $childSignature = [string]$progressActivity.ChildSignature
818
768
  $childAdvanced = ($childSignature -and $childSignature -ne $previousChildActivitySignature)
819
769
  $previousChildActivitySignature = $childSignature
820
770
 
821
771
  $effectiveStaleKillThreshold = Get-PrizmEffectiveStaleKillThreshold -ProgressFile $progressJson -BaseThreshold $staleKillThreshold
822
772
 
823
773
  # Check for error-loop: agent is actively producing output but results are
824
- # all read-offset errors or wasted calls.
774
+ # all read-offset errors or wasted calls. Only a newly advancing error
775
+ # signature counts; stale historical errors must not turn later normal output
776
+ # into a stale kill.
825
777
  $errorLoopDetected = $false
778
+ $errorLoopSignature = ''
826
779
  if ($effectiveStaleKillThreshold -gt 0 -and $growth -gt 0 -and (Test-Path $progressJson)) {
827
780
  try {
828
781
  $progress = Get-Content $progressJson -Raw | ConvertFrom-Json
@@ -833,17 +786,21 @@ function Invoke-PrizmPipeline {
833
786
  $_.type -in @("read_offset_overflow", "wasted_call")
834
787
  }).Count -eq 5
835
788
  if ($allBad) {
836
- $errorLoopDetected = $true
789
+ $errorLoopSignature = ($recent | ConvertTo-Json -Compress)
790
+ if ($previousErrorLoopSignature -and $errorLoopSignature -ne $previousErrorLoopSignature) {
791
+ $errorLoopDetected = $true
792
+ }
837
793
  }
838
794
  }
839
795
  } catch {
840
796
  # Ignore JSON parse errors — progress file may be incomplete or malformed
841
797
  }
842
798
  }
799
+ $previousErrorLoopSignature = $errorLoopSignature
843
800
 
844
801
  if ($errorLoopDetected) {
845
802
  $staleSeconds = $effectiveStaleKillThreshold
846
- } elseif ($growth -gt 0 -or $childAdvanced) {
803
+ } elseif ($growth -gt 0 -or $childAdvanced -or $progressAdvanced) {
847
804
  $staleSeconds = 0
848
805
  } else {
849
806
  $staleSeconds += $waitSeconds
@@ -946,16 +903,7 @@ function Invoke-PrizmPipeline {
946
903
  $itemListStatus = ''
947
904
  if ($status -eq 'success') {
948
905
  if (Test-PrizmGitDirty $paths.ProjectRoot) {
949
- if ($semanticCompletion) {
950
- $artifactDir = Join-Path $paths.PrizmkitDir "specs\$($semanticCompletion.Slug)"
951
- if (Save-PrizmPostCompletionDirtyArtifacts $paths.ProjectRoot $artifactDir $CurrentItemId $sessionId) {
952
- Write-PrizmWarn "Post-completion dirty changes preserved under $artifactDir"
953
- Write-PrizmWarn "They were not included in the finalized feature commit."
954
- } else {
955
- Write-PrizmWarn "Could not safely preserve post-completion dirty changes; preserving dev branch for manual finalization"
956
- $status = 'finalization_needed'
957
- }
958
- } elseif ($hadDirtyBaseline) {
906
+ if ($hadDirtyBaseline) {
959
907
  Write-PrizmInfo "Auto-committing pipeline bookkeeping artifacts only."
960
908
  Invoke-PrizmGitIncludeBookkeepingArtifacts $paths.ProjectRoot $stateDir $listPath
961
909
  } else {
@@ -121,6 +121,7 @@ $job = Start-Job -ScriptBlock {
121
121
  $elapsedSeconds = 0
122
122
  $staleSeconds = 0
123
123
  $previousLogSize = 0
124
+ $previousProgressSignature = ''
124
125
  $previousChildActivitySignature = ''
125
126
  $wasTimedOut = $false
126
127
  $wasStaleKilled = $false
@@ -136,12 +137,15 @@ while ($true) {
136
137
  $growth = $currentLogSize - $previousLogSize
137
138
  $previousLogSize = $currentLogSize
138
139
 
139
- $childActivity = Get-PrizmProgressChildActivity -ProgressFile $progressPath
140
- $childSignature = [string]$childActivity.Signature
140
+ $progressActivity = Get-PrizmProgressActivity -ProgressFile $progressPath
141
+ $progressSignature = [string]$progressActivity.ProgressSignature
142
+ $progressAdvanced = ($progressSignature -and $progressSignature -ne $previousProgressSignature)
143
+ $previousProgressSignature = $progressSignature
144
+ $childSignature = [string]$progressActivity.ChildSignature
141
145
  $childAdvanced = ($childSignature -and $childSignature -ne $previousChildActivitySignature)
142
146
  $previousChildActivitySignature = $childSignature
143
147
 
144
- if ($growth -gt 0 -or $childAdvanced) { $staleSeconds = 0 } else { $staleSeconds += $waitSeconds }
148
+ if ($growth -gt 0 -or $childAdvanced -or $progressAdvanced) { $staleSeconds = 0 } else { $staleSeconds += $waitSeconds }
145
149
 
146
150
  if ($timeoutSeconds -gt 0 -and $elapsedSeconds -ge $timeoutSeconds) {
147
151
  $wasTimedOut = $true
@@ -49,7 +49,6 @@ SESSION_STATUS_VALUES = [
49
49
  "commit_missing",
50
50
  "docs_missing",
51
51
  "merge_conflict",
52
- "finalization_needed",
53
52
  ]
54
53
 
55
54
  TERMINAL_STATUSES = {"completed", "failed", "skipped", "auto_skipped", "split"}
@@ -649,22 +648,6 @@ def action_update(args, feature_list_path, state_dir):
649
648
  fs["last_session_id"] = session_id
650
649
  fs["last_failed_session_id"] = session_id
651
650
 
652
- err = update_feature_in_list(feature_list_path, feature_id, new_status)
653
- if err:
654
- error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
655
- return
656
- elif session_status == "finalization_needed":
657
- # Runtime preserved dirty post-completion changes but could not safely
658
- # clean them for automatic merge. Preserve the dev branch and stop for
659
- # manual finalization instead of spending code retry budget.
660
- new_status = "failed"
661
- fs["degraded_reason"] = session_status
662
- fs["resume_from_phase"] = None
663
- fs["finalization_needed_count"] = fs.get("finalization_needed_count", 0) + 1
664
- if session_id:
665
- fs["last_session_id"] = session_id
666
- fs["last_failed_session_id"] = session_id
667
-
668
651
  err = update_feature_in_list(feature_list_path, feature_id, new_status)
669
652
  if err:
670
653
  error_out("Failed to update .prizmkit/plans/feature-list.json: {}".format(err))
@@ -736,9 +719,9 @@ def action_update(args, feature_list_path, state_dir):
736
719
  }
737
720
  if auto_skipped_features:
738
721
  summary["auto_skipped"] = [info["feature_id"] for info in auto_skipped_features]
739
- if session_status in ("commit_missing", "docs_missing", "merge_conflict", "finalization_needed"):
722
+ if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
740
723
  summary["degraded_reason"] = session_status
741
- summary["restart_policy"] = "manual_finalization" if session_status == "finalization_needed" else "finalization_retry"
724
+ summary["restart_policy"] = "finalization_retry"
742
725
  elif session_status == "infra_error":
743
726
  summary["restart_policy"] = "infra_retry"
744
727
  summary["infra_error_count"] = fs.get("infra_error_count", 0)
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.89",
2
+ "version": "1.1.91",
3
3
  "skills": {
4
4
  "prizm-kit": {
5
5
  "description": "Full-lifecycle dev toolkit. Covers spec-driven development, Prizm context docs, code quality, debugging, deployment, and knowledge management.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.89",
3
+ "version": "1.1.91",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {