prizmkit 1.1.69 → 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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/lib/common.sh +427 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +36 -0
- package/bundled/dev-pipeline/run-feature.sh +109 -29
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +11 -12
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +160 -3
- package/bundled/dev-pipeline/scripts/update-feature-status.py +27 -3
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +36 -22
- package/bundled/dev-pipeline/templates/agent-prompts/reviewer-review.md +1 -1
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +24 -21
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +13 -26
- package/bundled/dev-pipeline/templates/sections/ac-verification-checklist.md +4 -10
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -0
- package/bundled/dev-pipeline/templates/sections/feature-context.md +16 -11
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-auto.md +17 -26
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification-opencli.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-browser-verification.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +11 -0
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +11 -0
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +2 -9
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +8 -17
- package/bundled/dev-pipeline/templates/sections/phase-plan-lite.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/task-contract.md +34 -0
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +27 -46
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +27 -37
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +13 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +61 -1
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +299 -14
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +11 -12
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +160 -3
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +27 -3
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +36 -22
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +24 -21
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +13 -26
- package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +4 -10
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +1 -0
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +16 -11
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +22 -10
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +11 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +11 -0
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +2 -9
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +2 -9
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +8 -19
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/task-contract.md +34 -0
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +27 -46
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +27 -37
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -444,6 +444,433 @@ prizm_detect_infra_error() {
|
|
|
444
444
|
return 1
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
# Detect AI runtime/provider request failures that are not caused by generated
|
|
448
|
+
# project code. Unlike generic infra errors, these can be deterministic for the
|
|
449
|
+
# current transcript (for example context_too_large), so the runner must first
|
|
450
|
+
# check semantic completion before deciding whether to retry.
|
|
451
|
+
prizm_detect_ai_runtime_error() {
|
|
452
|
+
local session_log="${1:-}"
|
|
453
|
+
local progress_json="${2:-}"
|
|
454
|
+
|
|
455
|
+
if [[ -n "$progress_json" && -f "$progress_json" ]]; then
|
|
456
|
+
local fatal_error_code
|
|
457
|
+
fatal_error_code=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
458
|
+
import json
|
|
459
|
+
import sys
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
463
|
+
progress = json.load(fh)
|
|
464
|
+
except Exception:
|
|
465
|
+
raise SystemExit(0)
|
|
466
|
+
|
|
467
|
+
code = progress.get("fatal_error_code")
|
|
468
|
+
if code:
|
|
469
|
+
print(str(code))
|
|
470
|
+
PY
|
|
471
|
+
)
|
|
472
|
+
if [[ -n "$fatal_error_code" ]]; then
|
|
473
|
+
return 0
|
|
474
|
+
fi
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
local haystack=""
|
|
478
|
+
if [[ -n "$session_log" && -f "$session_log" ]]; then
|
|
479
|
+
haystack="$(tail -c 65536 "$session_log" 2>/dev/null || true)"
|
|
480
|
+
fi
|
|
481
|
+
if [[ -n "$progress_json" && -f "$progress_json" ]]; then
|
|
482
|
+
haystack+=$'\n'
|
|
483
|
+
haystack+="$(cat "$progress_json" 2>/dev/null || true)"
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
[[ -n "$haystack" ]] || return 1
|
|
487
|
+
|
|
488
|
+
if printf '%s' "$haystack" | grep -Eiq \
|
|
489
|
+
'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'; then
|
|
490
|
+
if printf '%s' "$haystack" | grep -Eiq \
|
|
491
|
+
'api error|invalid_request_error|api_error_status|api_error_code|status[[:space:]]*[:=]?[[:space:]]*(400|413)|last_result_is_error[[:space:]"'\'':=]+true|is_error[[:space:]"'\'':=]+true'; then
|
|
492
|
+
return 0
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
return 1
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
prizm_feature_slug_from_list() {
|
|
500
|
+
local feature_list="$1"
|
|
501
|
+
local feature_id="$2"
|
|
502
|
+
python3 - "$feature_list" "$feature_id" <<'PY'
|
|
503
|
+
import json
|
|
504
|
+
import re
|
|
505
|
+
import sys
|
|
506
|
+
|
|
507
|
+
feature_list, feature_id = sys.argv[1], sys.argv[2]
|
|
508
|
+
with open(feature_list, encoding="utf-8") as fh:
|
|
509
|
+
data = json.load(fh)
|
|
510
|
+
for feature in data.get("features", []):
|
|
511
|
+
if feature.get("id") == feature_id:
|
|
512
|
+
number = feature.get("id", "").replace("F-", "").replace("f-", "").zfill(3)
|
|
513
|
+
title = str(feature.get("title", "")).lower()
|
|
514
|
+
title = re.sub(r"[^a-z0-9\s-]", "", title)
|
|
515
|
+
title = re.sub(r"[\s]+", "-", title.strip())
|
|
516
|
+
title = re.sub(r"-+", "-", title).strip("-")
|
|
517
|
+
print(f"{number}-{title}" if title else number)
|
|
518
|
+
raise SystemExit(0)
|
|
519
|
+
raise SystemExit(1)
|
|
520
|
+
PY
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
prizm_checkpoint_all_complete() {
|
|
524
|
+
local checkpoint_file="$1"
|
|
525
|
+
[[ -f "$checkpoint_file" ]] || return 1
|
|
526
|
+
python3 - "$checkpoint_file" <<'PY'
|
|
527
|
+
import json
|
|
528
|
+
import sys
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
532
|
+
data = json.load(fh)
|
|
533
|
+
except Exception:
|
|
534
|
+
raise SystemExit(2)
|
|
535
|
+
steps = data.get("steps")
|
|
536
|
+
if not isinstance(steps, list) or not steps:
|
|
537
|
+
raise SystemExit(1)
|
|
538
|
+
for step in steps:
|
|
539
|
+
if not isinstance(step, dict) or step.get("status") not in ("completed", "skipped"):
|
|
540
|
+
raise SystemExit(1)
|
|
541
|
+
raise SystemExit(0)
|
|
542
|
+
PY
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Return the successful feature commit SHA if one exists in base_ref..HEAD.
|
|
546
|
+
# Prefer messages containing the feature ID. If checkpoint is complete, allow an
|
|
547
|
+
# older prompt variant only when the non-WIP commit identifies the feature title.
|
|
548
|
+
prizm_find_feature_commit() {
|
|
549
|
+
local project_root="$1"
|
|
550
|
+
local base_ref="$2"
|
|
551
|
+
local feature_id="$3"
|
|
552
|
+
local allow_title_fallback="${4:-false}"
|
|
553
|
+
local feature_title="${5:-}"
|
|
554
|
+
|
|
555
|
+
git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
|
|
556
|
+
|
|
557
|
+
local range="${base_ref}..HEAD"
|
|
558
|
+
if [[ -z "$base_ref" ]] || ! git -C "$project_root" rev-parse --verify "$base_ref" >/dev/null 2>&1; then
|
|
559
|
+
range="HEAD"
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
local feature_commit
|
|
563
|
+
feature_commit=$(git -C "$project_root" log "$range" --format='%H%x09%s' 2>/dev/null \
|
|
564
|
+
| awk -F '\t' -v fid="$feature_id" 'index($2, fid) > 0 && $2 !~ /^wip(\(|:)/ { print $1; exit }')
|
|
565
|
+
if [[ -n "$feature_commit" ]]; then
|
|
566
|
+
printf '%s\n' "$feature_commit"
|
|
567
|
+
return 0
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
if [[ "$allow_title_fallback" == "true" && -n "$feature_title" ]]; then
|
|
571
|
+
feature_commit=$(git -C "$project_root" log "$range" --format='%H%x09%s' 2>/dev/null \
|
|
572
|
+
| python3 -c '
|
|
573
|
+
import re
|
|
574
|
+
import sys
|
|
575
|
+
|
|
576
|
+
title = sys.argv[1]
|
|
577
|
+
|
|
578
|
+
def words(text):
|
|
579
|
+
return [w for w in re.split(r"[^a-z0-9]+", text.lower()) if len(w) >= 3]
|
|
580
|
+
|
|
581
|
+
title_words = words(title)
|
|
582
|
+
if not title_words:
|
|
583
|
+
raise SystemExit(1)
|
|
584
|
+
# Require all title words for short titles, or a strong majority for longer
|
|
585
|
+
# titles so punctuation/articles do not make older commits unmatchable.
|
|
586
|
+
required = len(title_words) if len(title_words) <= 3 else max(3, int(len(title_words) * 0.75 + 0.999))
|
|
587
|
+
for line in sys.stdin:
|
|
588
|
+
commit, sep, subject = line.rstrip("\n").partition("\t")
|
|
589
|
+
if not sep or re.match(r"^wip(\(|:)", subject):
|
|
590
|
+
continue
|
|
591
|
+
subject_words = set(words(subject))
|
|
592
|
+
if sum(1 for word in title_words if word in subject_words) >= required:
|
|
593
|
+
print(commit)
|
|
594
|
+
raise SystemExit(0)
|
|
595
|
+
raise SystemExit(1)
|
|
596
|
+
' "$feature_title")
|
|
597
|
+
if [[ -n "$feature_commit" ]]; then
|
|
598
|
+
printf '%s\n' "$feature_commit"
|
|
599
|
+
return 0
|
|
600
|
+
fi
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
return 1
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# Semantic completion means the durable workflow checkpoint is complete and a
|
|
607
|
+
# non-WIP feature commit exists. This intentionally runs before exit-code based
|
|
608
|
+
# failure classification so delayed post-commit model errors cannot strand a
|
|
609
|
+
# completed feature on its dev branch.
|
|
610
|
+
PRIZM_SEMANTIC_FEATURE_SLUG=""
|
|
611
|
+
PRIZM_SEMANTIC_COMMIT_SHA=""
|
|
612
|
+
prizm_feature_semantically_complete() {
|
|
613
|
+
local feature_list="$1"
|
|
614
|
+
local feature_id="$2"
|
|
615
|
+
local project_root="$3"
|
|
616
|
+
local base_ref="$4"
|
|
617
|
+
local prizmkit_dir="$5"
|
|
618
|
+
|
|
619
|
+
PRIZM_SEMANTIC_FEATURE_SLUG=""
|
|
620
|
+
PRIZM_SEMANTIC_COMMIT_SHA=""
|
|
621
|
+
|
|
622
|
+
local feature_slug
|
|
623
|
+
feature_slug=$(prizm_feature_slug_from_list "$feature_list" "$feature_id" 2>/dev/null) || return 1
|
|
624
|
+
local checkpoint_file="$prizmkit_dir/specs/${feature_slug}/workflow-checkpoint.json"
|
|
625
|
+
prizm_checkpoint_all_complete "$checkpoint_file" || return 1
|
|
626
|
+
|
|
627
|
+
local feature_title
|
|
628
|
+
feature_title=$(python3 - "$feature_list" "$feature_id" <<'PY' 2>/dev/null || true
|
|
629
|
+
import json
|
|
630
|
+
import sys
|
|
631
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
632
|
+
data = json.load(fh)
|
|
633
|
+
for feature in data.get("features", []):
|
|
634
|
+
if feature.get("id") == sys.argv[2]:
|
|
635
|
+
print(feature.get("title", ""))
|
|
636
|
+
break
|
|
637
|
+
PY
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
local commit_sha
|
|
641
|
+
commit_sha=$(prizm_find_feature_commit "$project_root" "$base_ref" "$feature_id" true "$feature_title" 2>/dev/null) || return 1
|
|
642
|
+
|
|
643
|
+
PRIZM_SEMANTIC_FEATURE_SLUG="$feature_slug"
|
|
644
|
+
PRIZM_SEMANTIC_COMMIT_SHA="$commit_sha"
|
|
645
|
+
return 0
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
prizm_preserve_post_completion_dirty() {
|
|
649
|
+
local project_root="$1"
|
|
650
|
+
local artifact_dir="$2"
|
|
651
|
+
local feature_id="$3"
|
|
652
|
+
local session_id="${4:-}"
|
|
653
|
+
|
|
654
|
+
git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
|
|
655
|
+
|
|
656
|
+
local dirty_status
|
|
657
|
+
dirty_status=$(git -C "$project_root" status --porcelain --untracked-files=all 2>/dev/null || true)
|
|
658
|
+
[[ -n "$dirty_status" ]] || return 0
|
|
659
|
+
|
|
660
|
+
mkdir -p "$artifact_dir" || return 1
|
|
661
|
+
|
|
662
|
+
local status_file="$artifact_dir/post-completion-status.txt"
|
|
663
|
+
local patch_file="$artifact_dir/post-completion-dirty.patch"
|
|
664
|
+
local staged_patch_file="$artifact_dir/post-completion-staged.patch"
|
|
665
|
+
local untracked_manifest="$artifact_dir/post-completion-untracked.txt"
|
|
666
|
+
local findings_file="$artifact_dir/post-completion-findings.md"
|
|
667
|
+
local untracked_dir="$artifact_dir/untracked"
|
|
668
|
+
|
|
669
|
+
printf '%s\n' "$dirty_status" > "$status_file" || return 1
|
|
670
|
+
git -C "$project_root" diff --binary > "$patch_file" 2>/dev/null || return 1
|
|
671
|
+
git -C "$project_root" diff --cached --binary > "$staged_patch_file" 2>/dev/null || return 1
|
|
672
|
+
|
|
673
|
+
: > "$untracked_manifest" || return 1
|
|
674
|
+
local untracked_tmp
|
|
675
|
+
untracked_tmp=$(mktemp 2>/dev/null || mktemp -t prizm-untracked) || return 1
|
|
676
|
+
git -C "$project_root" ls-files --others --exclude-standard -z > "$untracked_tmp" 2>/dev/null || {
|
|
677
|
+
rm -f "$untracked_tmp"
|
|
678
|
+
return 1
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if [[ -s "$untracked_tmp" ]]; then
|
|
682
|
+
mkdir -p "$untracked_dir" || {
|
|
683
|
+
rm -f "$untracked_tmp"
|
|
684
|
+
return 1
|
|
685
|
+
}
|
|
686
|
+
while IFS= read -r -d '' rel_path; do
|
|
687
|
+
[[ -n "$rel_path" ]] || continue
|
|
688
|
+
printf '%s\n' "$rel_path" >> "$untracked_manifest" || {
|
|
689
|
+
rm -f "$untracked_tmp"
|
|
690
|
+
return 1
|
|
691
|
+
}
|
|
692
|
+
local source_path="$project_root/$rel_path"
|
|
693
|
+
local dest_path="$untracked_dir/$rel_path"
|
|
694
|
+
mkdir -p "$(dirname "$dest_path")" || {
|
|
695
|
+
rm -f "$untracked_tmp"
|
|
696
|
+
return 1
|
|
697
|
+
}
|
|
698
|
+
if [[ -f "$source_path" ]]; then
|
|
699
|
+
cp -p "$source_path" "$dest_path" || {
|
|
700
|
+
rm -f "$untracked_tmp"
|
|
701
|
+
return 1
|
|
702
|
+
}
|
|
703
|
+
elif [[ -d "$source_path" ]]; then
|
|
704
|
+
mkdir -p "$dest_path" || {
|
|
705
|
+
rm -f "$untracked_tmp"
|
|
706
|
+
return 1
|
|
707
|
+
}
|
|
708
|
+
fi
|
|
709
|
+
done < "$untracked_tmp"
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
cat > "$findings_file" <<EOF
|
|
713
|
+
# Post-completion dirty changes preserved
|
|
714
|
+
|
|
715
|
+
- Feature: $feature_id
|
|
716
|
+
- Session: ${session_id:-unknown}
|
|
717
|
+
- Reason: workflow checkpoint and feature commit were already complete, but delayed post-commit activity left the working tree dirty.
|
|
718
|
+
|
|
719
|
+
## Recovery guidance
|
|
720
|
+
|
|
721
|
+
The finalized feature commit was kept unchanged for merge. Review these follow-up artifacts separately; do not assume they were merged:
|
|
722
|
+
|
|
723
|
+
- \`post-completion-status.txt\` — original dirty working tree status
|
|
724
|
+
- \`post-completion-dirty.patch\` — unstaged tracked changes
|
|
725
|
+
- \`post-completion-staged.patch\` — staged changes
|
|
726
|
+
- \`post-completion-untracked.txt\` and \`untracked/\` — untracked files copied before cleanup
|
|
727
|
+
EOF
|
|
728
|
+
|
|
729
|
+
git -C "$project_root" reset --hard >/dev/null 2>&1 || {
|
|
730
|
+
rm -f "$untracked_tmp"
|
|
731
|
+
return 1
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
while IFS= read -r -d '' rel_path; do
|
|
735
|
+
[[ -n "$rel_path" ]] || continue
|
|
736
|
+
case "$rel_path" in
|
|
737
|
+
.prizmkit/*) continue ;;
|
|
738
|
+
esac
|
|
739
|
+
rm -f "$project_root/$rel_path" 2>/dev/null || true
|
|
740
|
+
done < "$untracked_tmp"
|
|
741
|
+
rm -f "$untracked_tmp"
|
|
742
|
+
|
|
743
|
+
dirty_status=$(git -C "$project_root" status --porcelain --untracked-files=all 2>/dev/null | grep -v '^?? .prizmkit/' || true)
|
|
744
|
+
[[ -z "$dirty_status" ]] || return 1
|
|
745
|
+
|
|
746
|
+
return 0
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
prizm_synthesize_failure_log() {
|
|
750
|
+
local failure_log="$1"
|
|
751
|
+
local feature_id="$2"
|
|
752
|
+
local session_id="$3"
|
|
753
|
+
local session_status="$4"
|
|
754
|
+
local exit_code="$5"
|
|
755
|
+
local stale_kill_marker="$6"
|
|
756
|
+
local progress_json="$7"
|
|
757
|
+
local checkpoint_file="$8"
|
|
758
|
+
local project_root="$9"
|
|
759
|
+
local base_ref="${10:-}"
|
|
760
|
+
|
|
761
|
+
[[ -n "$failure_log" ]] || return 0
|
|
762
|
+
[[ -f "$failure_log" ]] && return 0
|
|
763
|
+
mkdir -p "$(dirname "$failure_log")" || return 0
|
|
764
|
+
|
|
765
|
+
local progress_summary="Progress data unavailable."
|
|
766
|
+
if [[ -f "$progress_json" ]]; then
|
|
767
|
+
progress_summary=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
768
|
+
import json
|
|
769
|
+
import sys
|
|
770
|
+
try:
|
|
771
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
772
|
+
data = json.load(fh)
|
|
773
|
+
except Exception as exc:
|
|
774
|
+
print(f"Progress parse error: {exc}")
|
|
775
|
+
raise SystemExit(0)
|
|
776
|
+
fields = [
|
|
777
|
+
("fatal_error_code", data.get("fatal_error_code")),
|
|
778
|
+
("api_error_status", data.get("api_error_status")),
|
|
779
|
+
("api_error_code", data.get("api_error_code")),
|
|
780
|
+
("current_phase", data.get("current_phase")),
|
|
781
|
+
("current_tool", data.get("current_tool")),
|
|
782
|
+
("last_text_snippet", data.get("last_text_snippet")),
|
|
783
|
+
("terminal_result_text", data.get("terminal_result_text")),
|
|
784
|
+
]
|
|
785
|
+
for key, value in fields:
|
|
786
|
+
if value not in (None, "", []):
|
|
787
|
+
text = str(value).replace("\n", " ")
|
|
788
|
+
print(f"- {key}: {text[:500]}")
|
|
789
|
+
PY
|
|
790
|
+
)
|
|
791
|
+
[[ -n "$progress_summary" ]] || progress_summary="Progress data contained no terminal fields."
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
local stale_summary="No stale-kill marker."
|
|
795
|
+
if [[ -f "$stale_kill_marker" ]]; then
|
|
796
|
+
stale_summary="$(cat "$stale_kill_marker" 2>/dev/null || true)"
|
|
797
|
+
fi
|
|
798
|
+
|
|
799
|
+
local checkpoint_summary="No checkpoint file found."
|
|
800
|
+
if [[ -f "$checkpoint_file" ]]; then
|
|
801
|
+
checkpoint_summary=$(python3 - "$checkpoint_file" <<'PY' 2>/dev/null || true
|
|
802
|
+
import json
|
|
803
|
+
import sys
|
|
804
|
+
try:
|
|
805
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
806
|
+
data = json.load(fh)
|
|
807
|
+
except Exception as exc:
|
|
808
|
+
print(f"Checkpoint parse error: {exc}")
|
|
809
|
+
raise SystemExit(0)
|
|
810
|
+
steps = data.get("steps") or []
|
|
811
|
+
complete = sum(1 for step in steps if isinstance(step, dict) and step.get("status") in ("completed", "skipped"))
|
|
812
|
+
print(f"{complete}/{len(steps)} steps completed_or_skipped")
|
|
813
|
+
for step in steps:
|
|
814
|
+
if isinstance(step, dict) and step.get("status") not in ("completed", "skipped"):
|
|
815
|
+
print(f"- incomplete: {step.get('id')} {step.get('skill')} = {step.get('status')}")
|
|
816
|
+
PY
|
|
817
|
+
)
|
|
818
|
+
fi
|
|
819
|
+
|
|
820
|
+
local latest_commit="unavailable"
|
|
821
|
+
local feature_commit="no"
|
|
822
|
+
if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
823
|
+
latest_commit=$(git -C "$project_root" rev-parse --short HEAD 2>/dev/null || echo unavailable)
|
|
824
|
+
if prizm_find_feature_commit "$project_root" "$base_ref" "$feature_id" false >/dev/null 2>&1; then
|
|
825
|
+
feature_commit="yes"
|
|
826
|
+
fi
|
|
827
|
+
fi
|
|
828
|
+
|
|
829
|
+
local dirty_summary="unavailable"
|
|
830
|
+
dirty_summary=$(git -C "$project_root" status --short 2>/dev/null || true)
|
|
831
|
+
[[ -n "$dirty_summary" ]] || dirty_summary="clean"
|
|
832
|
+
|
|
833
|
+
cat > "$failure_log" <<EOF
|
|
834
|
+
# Runtime-synthesized failure log
|
|
835
|
+
|
|
836
|
+
## Session
|
|
837
|
+
|
|
838
|
+
- feature_id: $feature_id
|
|
839
|
+
- session_id: ${session_id:-unknown}
|
|
840
|
+
- session_status: $session_status
|
|
841
|
+
- exit_code: $exit_code
|
|
842
|
+
|
|
843
|
+
## Stale kill marker
|
|
844
|
+
|
|
845
|
+
\`\`\`json
|
|
846
|
+
$stale_summary
|
|
847
|
+
\`\`\`
|
|
848
|
+
|
|
849
|
+
## Progress
|
|
850
|
+
|
|
851
|
+
$progress_summary
|
|
852
|
+
|
|
853
|
+
## Checkpoint
|
|
854
|
+
|
|
855
|
+
$checkpoint_summary
|
|
856
|
+
|
|
857
|
+
## Git state
|
|
858
|
+
|
|
859
|
+
- feature_commit_exists: $feature_commit
|
|
860
|
+
- latest_commit: $latest_commit
|
|
861
|
+
|
|
862
|
+
\`\`\`text
|
|
863
|
+
$dirty_summary
|
|
864
|
+
\`\`\`
|
|
865
|
+
|
|
866
|
+
## Recommended recovery action
|
|
867
|
+
|
|
868
|
+
- If this is an AI runtime/provider error before checkpoint completion, retry the session with a fresh context.
|
|
869
|
+
- If checkpoint completion and a feature commit both exist, inspect post-completion artifacts and finalize manually rather than rebuilding from scratch.
|
|
870
|
+
- If the working tree is dirty, preserve or review those changes before any reset or merge.
|
|
871
|
+
EOF
|
|
872
|
+
}
|
|
873
|
+
|
|
447
874
|
prizm_extract_update_new_status() {
|
|
448
875
|
python3 -c "
|
|
449
876
|
import json, sys
|
|
@@ -173,6 +173,42 @@ PY
|
|
|
173
173
|
fi
|
|
174
174
|
fi
|
|
175
175
|
|
|
176
|
+
# Fatal provider/runtime errors are terminal; do not wait for the
|
|
177
|
+
# stale window when progress.json already proves the model cannot
|
|
178
|
+
# continue (for example context_too_large).
|
|
179
|
+
if [[ -f "$progress_json" ]]; then
|
|
180
|
+
local fatal_error_code=""
|
|
181
|
+
fatal_error_code=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
182
|
+
import json
|
|
183
|
+
import sys
|
|
184
|
+
try:
|
|
185
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
186
|
+
progress = json.load(fh)
|
|
187
|
+
except Exception:
|
|
188
|
+
raise SystemExit(0)
|
|
189
|
+
code = progress.get("fatal_error_code") or ""
|
|
190
|
+
if code:
|
|
191
|
+
print(code)
|
|
192
|
+
PY
|
|
193
|
+
)
|
|
194
|
+
if [[ -n "$fatal_error_code" ]]; then
|
|
195
|
+
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}FATAL: ${fatal_error_code}${NC}"
|
|
196
|
+
local _marker_dir
|
|
197
|
+
_marker_dir="$(dirname "$session_log")"
|
|
198
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${fatal_error_code}\", \"fatal_error_code\": \"${fatal_error_code}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/fatal-error.json" 2>/dev/null || true
|
|
199
|
+
echo "{\"killed_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"reason\": \"${fatal_error_code}\", \"fatal_error_code\": \"${fatal_error_code}\", \"stale_seconds\": $stale_seconds, \"threshold\": $effective_stale_kill_threshold}" > "$_marker_dir/stale-kill.json" 2>/dev/null || true
|
|
200
|
+
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
201
|
+
local fatal_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|
|
202
|
+
if [[ $fatal_kill_grace_seconds -gt 0 ]]; then
|
|
203
|
+
sleep "$fatal_kill_grace_seconds"
|
|
204
|
+
fi
|
|
205
|
+
if kill -0 "$cli_pid" 2>/dev/null; then
|
|
206
|
+
kill -9 "$cli_pid" 2>/dev/null || true
|
|
207
|
+
fi
|
|
208
|
+
break
|
|
209
|
+
fi
|
|
210
|
+
fi
|
|
211
|
+
|
|
176
212
|
# Stale-kill: auto-terminate process if no progress for too long.
|
|
177
213
|
# Parent sessions can wait on spawned work; child transcript growth
|
|
178
214
|
# counts as progress above, while silent waits still use the active
|