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.
- package/bundled/VERSION.json +3 -3
- package/bundled/agents/prizm-dev-team-dev.md +11 -1
- package/bundled/dev-pipeline/lib/common.sh +427 -0
- package/bundled/dev-pipeline/lib/heartbeat.sh +101 -36
- package/bundled/dev-pipeline/run-feature.sh +109 -29
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +198 -3
- package/bundled/dev-pipeline/scripts/update-feature-status.py +27 -3
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +21 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +5 -9
- package/bundled/dev-pipeline/templates/sections/feature-context.md +3 -18
- 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-agent-suffix.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +6 -12
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +10 -3
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -0
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +4 -8
- package/bundled/dev-pipeline-windows/lib/common.ps1 +61 -1
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +325 -16
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +198 -3
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +27 -3
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +21 -0
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +27 -0
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +543 -14
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +664 -14
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +741 -14
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +1 -1
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +3 -3
- package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +3 -18
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +239 -40
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +75 -26
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +142 -36
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +13 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +12 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +7 -17
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +10 -3
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +3 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +7 -3
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +1 -3
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +1 -1
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +2 -2
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +13 -17
- package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +2 -4
- package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +1 -1
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
package/bundled/VERSION.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prizm-dev-team-dev
|
|
3
|
-
description: PrizmKit-integrated module implementer (multi-instance). Follows /prizmkit-implement workflow with TDD, marks tasks [x] in plan.md Tasks section
|
|
3
|
+
description: PrizmKit-integrated module implementer (multi-instance). Follows /prizmkit-implement workflow with TDD, marks tasks [x] in plan.md Tasks section. Works directly in the repository; uses git worktree only if explicitly instructed. Use when implementing specific feature modules.
|
|
4
4
|
tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskGet, TaskUpdate, TaskList, SendMessage
|
|
5
5
|
disallowedTools: Agent
|
|
6
6
|
model: inherit
|
|
@@ -86,6 +86,16 @@ DEV-17: DO NOT re-read source files already listed in context-snapshot.md Sectio
|
|
|
86
86
|
DEV-18: When tests fail, run `$TEST_CMD 2>&1 | tee /tmp/test-out.txt` ONCE, then grep `/tmp/test-out.txt` for failure details. Never re-run the full test suite just to apply a different grep filter to its output.
|
|
87
87
|
DEV-19: Before writing any `.prizmkit/prizm-docs/` file, check if it exists. If it exists: only update durable fields (KEY_FILES, INTERFACES, DEPENDENCIES, file counts, RULES, TRAPS, DECISIONS) — never overwrite the full file. Never add CHANGELOG, UPDATED/date fields, or workflow metadata. Only create new L2 docs for sub-modules you are actively creating in this session.
|
|
88
88
|
DEV-20: Internal tracking IDs are not product copy. Before writing UI text or UI-copy assertions, translate references like `F-003 guard` into product-language behavior such as `the high-risk guard`. Add regression coverage when a feature touches user-visible text.
|
|
89
|
+
DEV-21: Before any Read with offset + limit, compute offset + limit. If the last tool_result for this file shows it has N lines, offset MUST be < N. Never request an offset >= known file length.
|
|
90
|
+
DEV-22: When Edit fails with 'String to replace not found':
|
|
91
|
+
1. STOP editing immediately — do NOT retry the same Edit.
|
|
92
|
+
2. Run `grep -n` to locate the exact line of the target text.
|
|
93
|
+
3. Read with offset = max(grep_line - 20, 0), limit = 50.
|
|
94
|
+
4. Copy the exact text from the Read result into the Edit old_string.
|
|
95
|
+
5. Never guess or extrapolate an offset — grep first, then read, then edit.
|
|
96
|
+
DEV-23: If 3 consecutive Read calls to the same file return 'shorter than provided offset' or 'Wasted call', STOP all work on that file. Send ESCALATION with the file path, current line count, and the offsets you attempted. The orchestrator will provide the correct content.
|
|
97
|
+
DEV-24: Before editing a large file (>1000 lines), verify you know its current line count from the most recent tool_result. Old line counts from earlier turns may be stale if you have since edited the file.
|
|
98
|
+
DEV-25: After every 3 successful Edit operations on a file, run the relevant test command for that file once to validate your changes compile and behave correctly. Do not defer all testing to the end.
|
|
89
99
|
```
|
|
90
100
|
|
|
91
101
|
### Workflow
|
|
@@ -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
|
|
@@ -90,9 +90,79 @@ PY
|
|
|
90
90
|
fi
|
|
91
91
|
prev_child_activity_signature="$child_activity_signature"
|
|
92
92
|
|
|
93
|
+
local effective_stale_kill_threshold="$stale_kill_threshold"
|
|
94
|
+
if [[ $stale_kill_threshold -gt 0 && -f "$progress_json" ]]; then
|
|
95
|
+
local codex_wait_threshold
|
|
96
|
+
codex_wait_threshold=$(python3 - "$progress_json" "$stale_kill_threshold" <<'PY' 2>/dev/null || true
|
|
97
|
+
import json
|
|
98
|
+
import os
|
|
99
|
+
import sys
|
|
100
|
+
|
|
101
|
+
progress_path = sys.argv[1]
|
|
102
|
+
base_threshold = int(sys.argv[2])
|
|
103
|
+
|
|
104
|
+
with open(progress_path, "r", encoding="utf-8") as fh:
|
|
105
|
+
progress = json.load(fh)
|
|
106
|
+
|
|
107
|
+
spawn_count = 0
|
|
108
|
+
for tool in progress.get("tool_calls", []):
|
|
109
|
+
if isinstance(tool, dict) and tool.get("name") == "spawn_agent":
|
|
110
|
+
try:
|
|
111
|
+
spawn_count += int(tool.get("count", 0))
|
|
112
|
+
except (TypeError, ValueError):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
progress.get("event_format") == "codex-json"
|
|
117
|
+
and progress.get("current_tool") == "wait"
|
|
118
|
+
and spawn_count > 0
|
|
119
|
+
):
|
|
120
|
+
configured = os.environ.get("CODEX_WAIT_STALE_KILL_THRESHOLD", "")
|
|
121
|
+
try:
|
|
122
|
+
wait_threshold = int(configured)
|
|
123
|
+
except ValueError:
|
|
124
|
+
wait_threshold = max(base_threshold * 4, 3600)
|
|
125
|
+
if wait_threshold > base_threshold:
|
|
126
|
+
print(wait_threshold)
|
|
127
|
+
PY
|
|
128
|
+
)
|
|
129
|
+
if [[ "$codex_wait_threshold" =~ ^[0-9]+$ && "$codex_wait_threshold" -gt "$stale_kill_threshold" ]]; then
|
|
130
|
+
effective_stale_kill_threshold="$codex_wait_threshold"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Check for error-loop: agent is actively producing output but results are
|
|
135
|
+
# all read-offset errors or wasted calls. This is a stuck agent that appears
|
|
136
|
+
# "active" by log growth but is accomplishing nothing.
|
|
137
|
+
local error_loop_detected=false
|
|
138
|
+
if [[ $effective_stale_kill_threshold -gt 0 && $growth -gt 0 && -f "$progress_json" ]]; then
|
|
139
|
+
local error_loop_flag
|
|
140
|
+
error_loop_flag=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
141
|
+
import json, sys
|
|
142
|
+
try:
|
|
143
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
144
|
+
progress = json.load(fh)
|
|
145
|
+
except Exception:
|
|
146
|
+
raise SystemExit(0)
|
|
147
|
+
errors = progress.get("errors", [])
|
|
148
|
+
if isinstance(errors, list) and len(errors) >= 5:
|
|
149
|
+
recent = errors[-5:]
|
|
150
|
+
if all(isinstance(e, dict) and e.get("type") in ("read_offset_overflow", "wasted_call") for e in recent):
|
|
151
|
+
print("error_loop")
|
|
152
|
+
PY
|
|
153
|
+
)
|
|
154
|
+
if [[ "$error_loop_flag" == "error_loop" ]]; then
|
|
155
|
+
error_loop_detected=true
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
93
159
|
# Track progress staleness. Parent sessions can sit in a wait/polling
|
|
94
160
|
# tool while child transcripts keep growing, so child activity counts.
|
|
95
|
-
|
|
161
|
+
# Error loops bypass normal growth-as-progress because the log is only
|
|
162
|
+
# growing with repeated failed reads or wasted calls.
|
|
163
|
+
if [[ "$error_loop_detected" == "true" ]]; then
|
|
164
|
+
stale_seconds=$effective_stale_kill_threshold
|
|
165
|
+
elif [[ $growth -eq 0 && $child_growth -eq 0 ]]; then
|
|
96
166
|
stale_seconds=$((stale_seconds + heartbeat_interval))
|
|
97
167
|
else
|
|
98
168
|
stale_seconds=0
|
|
@@ -132,44 +202,39 @@ PY
|
|
|
132
202
|
status_icon="${YELLOW}⏸${NC}"
|
|
133
203
|
fi
|
|
134
204
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
205
|
+
# Fatal provider/runtime errors are terminal; do not wait for the
|
|
206
|
+
# stale window when progress.json already proves the model cannot
|
|
207
|
+
# continue (for example context_too_large).
|
|
208
|
+
if [[ -f "$progress_json" ]]; then
|
|
209
|
+
local fatal_error_code=""
|
|
210
|
+
fatal_error_code=$(python3 - "$progress_json" <<'PY' 2>/dev/null || true
|
|
139
211
|
import json
|
|
140
|
-
import os
|
|
141
212
|
import sys
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for tool in progress.get("tool_calls", []):
|
|
151
|
-
if isinstance(tool, dict) and tool.get("name") == "spawn_agent":
|
|
152
|
-
try:
|
|
153
|
-
spawn_count += int(tool.get("count", 0))
|
|
154
|
-
except (TypeError, ValueError):
|
|
155
|
-
pass
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
progress.get("event_format") == "codex-json"
|
|
159
|
-
and progress.get("current_tool") == "wait"
|
|
160
|
-
and spawn_count > 0
|
|
161
|
-
):
|
|
162
|
-
configured = os.environ.get("CODEX_WAIT_STALE_KILL_THRESHOLD", "")
|
|
163
|
-
try:
|
|
164
|
-
wait_threshold = int(configured)
|
|
165
|
-
except ValueError:
|
|
166
|
-
wait_threshold = max(base_threshold * 4, 3600)
|
|
167
|
-
if wait_threshold > base_threshold:
|
|
168
|
-
print(wait_threshold)
|
|
213
|
+
try:
|
|
214
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
215
|
+
progress = json.load(fh)
|
|
216
|
+
except Exception:
|
|
217
|
+
raise SystemExit(0)
|
|
218
|
+
code = progress.get("fatal_error_code") or ""
|
|
219
|
+
if code:
|
|
220
|
+
print(code)
|
|
169
221
|
PY
|
|
170
|
-
|
|
171
|
-
if [[
|
|
172
|
-
|
|
222
|
+
)
|
|
223
|
+
if [[ -n "$fatal_error_code" ]]; then
|
|
224
|
+
echo -e " ${RED}[HEARTBEAT]${NC} ${mins}m${secs}s | log: ${size_display} | ${RED}FATAL: ${fatal_error_code}${NC}"
|
|
225
|
+
local _marker_dir
|
|
226
|
+
_marker_dir="$(dirname "$session_log")"
|
|
227
|
+
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
|
|
228
|
+
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
|
|
229
|
+
kill -TERM "$cli_pid" 2>/dev/null || true
|
|
230
|
+
local fatal_kill_grace_seconds="${STALE_KILL_GRACE_SECONDS:-10}"
|
|
231
|
+
if [[ $fatal_kill_grace_seconds -gt 0 ]]; then
|
|
232
|
+
sleep "$fatal_kill_grace_seconds"
|
|
233
|
+
fi
|
|
234
|
+
if kill -0 "$cli_pid" 2>/dev/null; then
|
|
235
|
+
kill -9 "$cli_pid" 2>/dev/null || true
|
|
236
|
+
fi
|
|
237
|
+
break
|
|
173
238
|
fi
|
|
174
239
|
fi
|
|
175
240
|
|