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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/lib/common.sh +0 -102
- package/bundled/dev-pipeline/lib/heartbeat.sh +56 -22
- package/bundled/dev-pipeline/run-feature.sh +5 -22
- package/bundled/dev-pipeline/scripts/update-feature-status.py +2 -19
- package/bundled/dev-pipeline-windows/lib/common.ps1 +61 -13
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +18 -70
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +7 -3
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +2 -19
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "$
|
|
99
|
-
IFS=$'\
|
|
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("
|
|
219
|
+
print(json.dumps(recent, sort_keys=True, separators=(",", ":")))
|
|
191
220
|
PY
|
|
192
221
|
)
|
|
193
|
-
if [[ "$error_loop_flag"
|
|
194
|
-
|
|
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
|
|
200
|
-
#
|
|
201
|
-
#
|
|
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
|
|
205
|
-
# primary progress
|
|
206
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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"
|
|
722
|
+
if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
740
723
|
summary["degraded_reason"] = session_status
|
|
741
|
-
summary["restart_policy"] = "
|
|
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-
|
|
470
|
+
function Get-PrizmProgressActivity {
|
|
471
471
|
param([string]$ProgressFile)
|
|
472
472
|
|
|
473
473
|
$empty = [pscustomobject]@{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
$
|
|
487
|
+
$childSignature = ''
|
|
487
488
|
if ($progress.PSObject.Properties['child_activity_signature'] -and $progress.child_activity_signature) {
|
|
488
|
-
$
|
|
489
|
+
$childSignature = [string]$progress.child_activity_signature
|
|
489
490
|
}
|
|
490
491
|
|
|
491
|
-
$
|
|
492
|
+
$childTotalBytes = [int64]0
|
|
492
493
|
if ($progress.PSObject.Properties['child_total_bytes']) {
|
|
493
|
-
[int64]::TryParse([string]$progress.child_total_bytes, [ref]$
|
|
494
|
+
[int64]::TryParse([string]$progress.child_total_bytes, [ref]$childTotalBytes) | Out-Null
|
|
494
495
|
}
|
|
495
496
|
|
|
496
|
-
$
|
|
497
|
+
$childSessionCount = 0
|
|
497
498
|
if ($progress.PSObject.Properties['child_session_files'] -and $progress.child_session_files) {
|
|
498
|
-
$
|
|
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 = $
|
|
503
|
-
TotalBytes = $
|
|
504
|
-
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
|
-
$
|
|
817
|
-
$
|
|
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
|
-
$
|
|
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 ($
|
|
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
|
-
$
|
|
140
|
-
$
|
|
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"
|
|
722
|
+
if session_status in ("commit_missing", "docs_missing", "merge_conflict"):
|
|
740
723
|
summary["degraded_reason"] = session_status
|
|
741
|
-
summary["restart_policy"] = "
|
|
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)
|