prizmkit 1.1.120 → 1.1.121
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/prizmkit_runtime/runner_classification.py +23 -25
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +8 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +10 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +43 -29
- package/bundled/dev-pipeline/scripts/continuation.py +51 -6
- package/bundled/dev-pipeline/scripts/update-bug-status.py +57 -14
- package/bundled/dev-pipeline/scripts/update-feature-status.py +54 -5
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +57 -14
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +2 -2
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +2 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +2 -2
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +3 -3
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +4 -4
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +3 -3
- package/bundled/dev-pipeline/tests/test_auto_skip.py +5 -5
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +10 -3
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +9 -2
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +9 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +222 -18
- package/bundled/dev-pipeline/tests/test_unified_cli.py +14 -10
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/prizmkit-code-review/SKILL.md +49 -48
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +13 -8
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +46 -32
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +90 -0
- package/bundled/skills/prizmkit-test/SKILL.md +159 -155
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +420 -0
- package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +116 -0
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +112 -0
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +97 -0
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +76 -192
- package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +65 -0
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +194 -0
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +78 -0
- package/bundled/skills/prizmkit-test/references/examples.md +65 -108
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +10 -7
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +90 -82
- package/bundled/skills/prizmkit-test/references/test-report-template.md +77 -98
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +97 -0
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +723 -0
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +1426 -0
- package/package.json +1 -1
- package/bundled/skills/prizmkit-test/scripts/validate_boundary_report.py +0 -347
package/bundled/VERSION.json
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
+
import json
|
|
6
7
|
import subprocess
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from .checkpoint_state import load_checkpoint_state
|
|
10
|
-
from .gitops import
|
|
11
|
+
from .gitops import git_status_safe, run_git_command
|
|
11
12
|
from .runner_models import PromptGenerationResult, SessionClassification
|
|
12
13
|
from .sessions import AISessionResult
|
|
13
14
|
|
|
@@ -54,8 +55,6 @@ def classify_session(
|
|
|
54
55
|
reason=f"invalid_workflow_checkpoint:{invalid_checkpoint.error_code}",
|
|
55
56
|
progress_fingerprint=fingerprint,
|
|
56
57
|
)
|
|
57
|
-
has_commit_since_base = _has_commit_since(project_root, base_head)
|
|
58
|
-
|
|
59
58
|
if _is_terminal_success(result) and _has_completed_workflow_artifact(prompt):
|
|
60
59
|
return SessionClassification(
|
|
61
60
|
"success",
|
|
@@ -63,7 +62,7 @@ def classify_session(
|
|
|
63
62
|
progress_fingerprint=fingerprint,
|
|
64
63
|
)
|
|
65
64
|
|
|
66
|
-
if _has_late_runtime_error_after_completion(result, prompt
|
|
65
|
+
if _has_late_runtime_error_after_completion(result, prompt):
|
|
67
66
|
return SessionClassification(
|
|
68
67
|
"success",
|
|
69
68
|
reason="completed_checkpoint_with_late_runtime_error",
|
|
@@ -71,8 +70,6 @@ def classify_session(
|
|
|
71
70
|
)
|
|
72
71
|
|
|
73
72
|
if _is_context_overflow(result):
|
|
74
|
-
if has_commit_since_base:
|
|
75
|
-
return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
|
|
76
73
|
no_progress_count = _next_no_progress_count(previous_fingerprint, fingerprint, previous_no_progress_count)
|
|
77
74
|
if no_progress_count >= 2:
|
|
78
75
|
return SessionClassification(
|
|
@@ -90,19 +87,14 @@ def classify_session(
|
|
|
90
87
|
no_progress_count=no_progress_count,
|
|
91
88
|
)
|
|
92
89
|
|
|
93
|
-
if _is_infra_error(result):
|
|
94
|
-
return SessionClassification("infra_error", reason="ai_runtime_or_provider_error", progress_fingerprint=fingerprint)
|
|
95
90
|
if result.termination_reason == "stale_session":
|
|
96
91
|
return SessionClassification("crashed", reason="stale_session", progress_fingerprint=fingerprint)
|
|
97
92
|
if result.timed_out or result.termination_reason == "timeout":
|
|
98
93
|
return SessionClassification("timed_out", reason="timeout", progress_fingerprint=fingerprint)
|
|
94
|
+
if _is_infra_error(result):
|
|
95
|
+
return SessionClassification("infra_error", reason="ai_runtime_or_provider_error", progress_fingerprint=fingerprint)
|
|
99
96
|
if result.exit_code not in (0, None):
|
|
100
97
|
return SessionClassification("crashed", reason=f"exit_code={result.exit_code}", progress_fingerprint=fingerprint)
|
|
101
|
-
if has_commit_since_base:
|
|
102
|
-
return SessionClassification("success", reason="commit_since_base", progress_fingerprint=fingerprint)
|
|
103
|
-
if _has_workspace_changes(project_root):
|
|
104
|
-
_auto_commit_workspace(project_root)
|
|
105
|
-
return SessionClassification("success", reason="auto_commit_uncommitted_changes", progress_fingerprint=fingerprint)
|
|
106
98
|
return SessionClassification("crashed", reason="no_durable_success", progress_fingerprint=fingerprint)
|
|
107
99
|
|
|
108
100
|
|
|
@@ -138,9 +130,8 @@ def _is_terminal_success(result: AISessionResult) -> bool:
|
|
|
138
130
|
def _has_late_runtime_error_after_completion(
|
|
139
131
|
result: AISessionResult,
|
|
140
132
|
prompt: PromptGenerationResult | None,
|
|
141
|
-
has_commit_since_base: bool,
|
|
142
133
|
) -> bool:
|
|
143
|
-
if not
|
|
134
|
+
if not _has_completed_workflow_artifact(prompt):
|
|
144
135
|
return False
|
|
145
136
|
return _is_context_overflow(result) or _is_infra_error(result)
|
|
146
137
|
|
|
@@ -148,6 +139,8 @@ def _has_late_runtime_error_after_completion(
|
|
|
148
139
|
def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> bool:
|
|
149
140
|
if prompt is None:
|
|
150
141
|
return False
|
|
142
|
+
if _invalid_existing_workflow_checkpoint(prompt) is not None:
|
|
143
|
+
return False
|
|
151
144
|
if prompt.checkpoint_path and prompt.checkpoint_path.is_file():
|
|
152
145
|
state = load_checkpoint_state(prompt.checkpoint_path)
|
|
153
146
|
return state.valid and state.all_completed
|
|
@@ -170,7 +163,21 @@ def has_completed_artifact_path(artifact_path: Path | None) -> bool:
|
|
|
170
163
|
if checkpoint.is_file():
|
|
171
164
|
state = load_checkpoint_state(checkpoint)
|
|
172
165
|
return state.valid and state.all_completed
|
|
173
|
-
return (artifact_path / "completion-summary.json")
|
|
166
|
+
return _has_valid_completion_summary(artifact_path / "completion-summary.json")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _has_valid_completion_summary(path: Path) -> bool:
|
|
170
|
+
"""Return whether a completion summary contains usable durable notes."""
|
|
171
|
+
try:
|
|
172
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
173
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
174
|
+
return False
|
|
175
|
+
if not isinstance(data, dict):
|
|
176
|
+
return False
|
|
177
|
+
notes = data.get("completion_notes")
|
|
178
|
+
return isinstance(notes, list) and bool(notes) and all(
|
|
179
|
+
isinstance(note, str) and note.strip() for note in notes
|
|
180
|
+
)
|
|
174
181
|
|
|
175
182
|
|
|
176
183
|
def _invalid_existing_workflow_checkpoint(prompt: PromptGenerationResult | None):
|
|
@@ -226,15 +233,6 @@ def _has_commit_since(project_root: Path, base_head: str) -> bool:
|
|
|
226
233
|
return _has_commit_range(project_root, base_head, "HEAD")
|
|
227
234
|
|
|
228
235
|
|
|
229
|
-
def _has_workspace_changes(project_root: Path) -> bool:
|
|
230
|
-
return bool(git_status_safe(project_root).strip())
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _auto_commit_workspace(project_root: Path) -> None:
|
|
234
|
-
run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
|
|
235
|
-
run_git_command(project_root, ("commit", "--no-verify", "-m", "chore: save runner session changes"))
|
|
236
|
-
|
|
237
|
-
|
|
238
236
|
def _tail(path: Path, max_bytes: int = 8192) -> str:
|
|
239
237
|
try:
|
|
240
238
|
data = path.read_bytes()
|
|
@@ -133,17 +133,23 @@ class SessionPaths:
|
|
|
133
133
|
|
|
134
134
|
@dataclass(frozen=True)
|
|
135
135
|
class ContinuationState:
|
|
136
|
-
"""
|
|
136
|
+
"""Provider-neutral durable handoff inputs for the next AI session."""
|
|
137
137
|
|
|
138
138
|
active: bool = False
|
|
139
139
|
previous_session_id: str = ""
|
|
140
|
+
failure_classification: str = ""
|
|
140
141
|
continuation_count: int = 0
|
|
141
142
|
context_overflow_count: int = 0
|
|
143
|
+
retry_count: int = 0
|
|
144
|
+
infra_error_count: int = 0
|
|
145
|
+
max_retries: int = 0
|
|
146
|
+
max_infra_retries: int = 0
|
|
142
147
|
no_progress_count: int = 0
|
|
148
|
+
progress_fingerprint: Mapping[str, object] = field(default_factory=dict)
|
|
143
149
|
summary_path: Path | None = None
|
|
144
150
|
active_dev_branch: str = ""
|
|
145
151
|
base_branch: str = ""
|
|
146
|
-
fatal_error_code: str = "
|
|
152
|
+
fatal_error_code: str = ""
|
|
147
153
|
|
|
148
154
|
|
|
149
155
|
@dataclass(frozen=True)
|
|
@@ -50,11 +50,18 @@ def generate_prompt(
|
|
|
50
50
|
if invocation.mode:
|
|
51
51
|
command.extend(["--mode", invocation.mode])
|
|
52
52
|
if continuation:
|
|
53
|
-
command.extend(["--continuation-mode", "
|
|
53
|
+
command.extend(["--continuation-mode", "ai_error"])
|
|
54
54
|
for flag, key in (
|
|
55
55
|
("--previous-session-id", "previous_session_id"),
|
|
56
|
+
("--failure-classification", "failure_classification"),
|
|
56
57
|
("--continuation-count", "continuation_count"),
|
|
57
58
|
("--context-overflow-count", "context_overflow_count"),
|
|
59
|
+
("--code-retry-count", "retry_count"),
|
|
60
|
+
("--infra-error-count", "infra_error_count"),
|
|
61
|
+
("--max-code-retries", "max_retries"),
|
|
62
|
+
("--max-infra-retries", "max_infra_retries"),
|
|
63
|
+
("--no-progress-count", "no_progress_count"),
|
|
64
|
+
("--progress-fingerprint", "progress_fingerprint"),
|
|
58
65
|
("--task-type", "task_type"),
|
|
59
66
|
("--active-dev-branch", "active_dev_branch"),
|
|
60
67
|
("--base-branch", "base_branch"),
|
|
@@ -62,6 +69,8 @@ def generate_prompt(
|
|
|
62
69
|
):
|
|
63
70
|
value = continuation.get(key)
|
|
64
71
|
if value not in (None, ""):
|
|
72
|
+
if key == "progress_fingerprint":
|
|
73
|
+
value = json.dumps(value, sort_keys=True)
|
|
65
74
|
command.extend([flag, str(value)])
|
|
66
75
|
|
|
67
76
|
completed = subprocess.run(
|
|
@@ -531,8 +531,11 @@ def _process_item(
|
|
|
531
531
|
previous_no_progress_count = int(metadata.get("no_progress_count") or 0)
|
|
532
532
|
continuation_pending = bool(metadata.get("continuation_pending"))
|
|
533
533
|
previous_session_id = str(metadata.get("previous_session_id") or metadata.get("last_context_overflow_session_id") or "")
|
|
534
|
+
failure_classification = str(metadata.get("continuation_reason") or "context_overflow")
|
|
534
535
|
continuation_count = int(metadata.get("continuation_count") or 0)
|
|
535
536
|
context_overflow_count = int(metadata.get("context_overflow_count") or 0)
|
|
537
|
+
retry_count = int(metadata.get("retry_count") or 0)
|
|
538
|
+
infra_error_count = int(metadata.get("infra_error_count") or 0)
|
|
536
539
|
|
|
537
540
|
while True:
|
|
538
541
|
session_id = _session_id(item_id)
|
|
@@ -541,8 +544,15 @@ def _process_item(
|
|
|
541
544
|
if continuation_pending:
|
|
542
545
|
continuation = {
|
|
543
546
|
"previous_session_id": previous_session_id,
|
|
547
|
+
"failure_classification": failure_classification,
|
|
544
548
|
"continuation_count": continuation_count,
|
|
545
549
|
"context_overflow_count": context_overflow_count,
|
|
550
|
+
"retry_count": retry_count,
|
|
551
|
+
"infra_error_count": infra_error_count,
|
|
552
|
+
"max_retries": _max_retries(invocation),
|
|
553
|
+
"max_infra_retries": invocation.max_infra_retries,
|
|
554
|
+
"no_progress_count": previous_no_progress_count,
|
|
555
|
+
"progress_fingerprint": previous_fingerprint or {},
|
|
546
556
|
"task_type": family.task_type,
|
|
547
557
|
"active_dev_branch": branch_name,
|
|
548
558
|
"base_branch": base_branch,
|
|
@@ -554,7 +564,7 @@ def _process_item(
|
|
|
554
564
|
item_id=item_id,
|
|
555
565
|
session_id=session_id,
|
|
556
566
|
run_id="run-python",
|
|
557
|
-
retry_count=
|
|
567
|
+
retry_count=retry_count,
|
|
558
568
|
session_paths=session_paths,
|
|
559
569
|
project_root=paths.project_root,
|
|
560
570
|
execution_root=execution_root,
|
|
@@ -599,34 +609,8 @@ def _process_item(
|
|
|
599
609
|
previous_no_progress_count=previous_no_progress_count,
|
|
600
610
|
)
|
|
601
611
|
_emit_session_summary(session_result, session_paths, classification)
|
|
602
|
-
if classification.session_status == "context_overflow":
|
|
603
|
-
summary_path = _continuation_summary_path(execution_root, family, item_id, prompt)
|
|
604
|
-
update = update_item(
|
|
605
|
-
status_family,
|
|
606
|
-
status_invocation,
|
|
607
|
-
item_id,
|
|
608
|
-
"context_overflow",
|
|
609
|
-
session_id,
|
|
610
|
-
status_root,
|
|
611
|
-
active_dev_branch=branch_name,
|
|
612
|
-
base_branch=base_branch,
|
|
613
|
-
last_fatal_error_code=classification.fatal_error_code or "context_overflow",
|
|
614
|
-
continuation_summary_path=summary_path,
|
|
615
|
-
no_progress_count=classification.no_progress_count,
|
|
616
|
-
progress_fingerprint=classification.progress_fingerprint,
|
|
617
|
-
)
|
|
618
|
-
if not update.ok:
|
|
619
|
-
raise RuntimeError(update.text)
|
|
620
|
-
_emit_update_summary(family, item_id, update)
|
|
621
|
-
previous_fingerprint = classification.progress_fingerprint
|
|
622
|
-
previous_no_progress_count = classification.no_progress_count
|
|
623
|
-
continuation_pending = True
|
|
624
|
-
previous_session_id = session_id
|
|
625
|
-
continuation_count += 1
|
|
626
|
-
context_overflow_count += 1
|
|
627
|
-
continue
|
|
628
|
-
|
|
629
612
|
final_session_status = classification.session_status
|
|
613
|
+
summary_path = _continuation_summary_path(execution_root, family, item_id, prompt)
|
|
630
614
|
|
|
631
615
|
_write_session_status(
|
|
632
616
|
session_paths,
|
|
@@ -651,6 +635,7 @@ def _process_item(
|
|
|
651
635
|
active_dev_branch=branch_name,
|
|
652
636
|
base_branch=base_branch,
|
|
653
637
|
last_fatal_error_code=classification.fatal_error_code,
|
|
638
|
+
continuation_summary_path=summary_path,
|
|
654
639
|
no_progress_count=classification.no_progress_count,
|
|
655
640
|
progress_fingerprint=classification.progress_fingerprint,
|
|
656
641
|
)
|
|
@@ -658,6 +643,22 @@ def _process_item(
|
|
|
658
643
|
raise RuntimeError(update.text)
|
|
659
644
|
_emit_update_summary(family, item_id, update)
|
|
660
645
|
new_status = _status_from_update(update)
|
|
646
|
+
update_data = update.data if isinstance(update.data, dict) else {}
|
|
647
|
+
retry_count = int(update_data.get("retry_count", retry_count) or 0)
|
|
648
|
+
infra_error_count = int(update_data.get("infra_error_count", infra_error_count) or 0)
|
|
649
|
+
|
|
650
|
+
if _is_retryable_ai_outcome(final_session_status, new_status):
|
|
651
|
+
previous_fingerprint = classification.progress_fingerprint
|
|
652
|
+
previous_no_progress_count = classification.no_progress_count
|
|
653
|
+
continuation_pending = True
|
|
654
|
+
previous_session_id = session_id
|
|
655
|
+
failure_classification = classification.reason or final_session_status
|
|
656
|
+
continuation_count = int(update_data.get("continuation_count", continuation_count + 1) or 0)
|
|
657
|
+
context_overflow_count = int(update_data.get("context_overflow_count", context_overflow_count) or 0)
|
|
658
|
+
_emit_info(f"Retryable AI outcome persisted ({failure_classification}); continuing in 5s...")
|
|
659
|
+
time.sleep(5)
|
|
660
|
+
continue
|
|
661
|
+
|
|
661
662
|
if final_session_status != "success":
|
|
662
663
|
commit_final_bookkeeping(status_root, status_family, item_id, new_status)
|
|
663
664
|
_save_failure_wip(execution_root, family, item_id, new_status, worktree_runtime)
|
|
@@ -694,6 +695,19 @@ def _process_item(
|
|
|
694
695
|
raise
|
|
695
696
|
|
|
696
697
|
|
|
698
|
+
def _is_retryable_ai_outcome(session_status: str, new_status: str) -> bool:
|
|
699
|
+
"""Return whether a persisted post-launch outcome should continue in-process."""
|
|
700
|
+
if session_status == "success" or session_status == "stalled_context_continuation":
|
|
701
|
+
return False
|
|
702
|
+
return new_status in {"in_progress", "pending"} and session_status in {
|
|
703
|
+
"context_overflow",
|
|
704
|
+
"infra_error",
|
|
705
|
+
"crashed",
|
|
706
|
+
"timed_out",
|
|
707
|
+
"failed",
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
|
|
697
711
|
def _finalize_recovered_success(
|
|
698
712
|
family: RunnerFamily,
|
|
699
713
|
item_id: str,
|
|
@@ -1012,7 +1026,7 @@ def _default_branch_name(family: RunnerFamily, item_id: str) -> str:
|
|
|
1012
1026
|
|
|
1013
1027
|
|
|
1014
1028
|
def _session_id(item_id: str) -> str:
|
|
1015
|
-
return f"{item_id}-{time.strftime('%Y%m%d%H%M%S')}"
|
|
1029
|
+
return f"{item_id}-{time.strftime('%Y%m%d%H%M%S')}-{time.time_ns()}"
|
|
1016
1030
|
|
|
1017
1031
|
|
|
1018
1032
|
def _continuation_summary_path(project_root: Path, family: RunnerFamily, item_id: str, prompt: PromptGenerationResult | None) -> Path:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Continuation prompt and durable handoff helpers for pipeline prompt generators."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
|
|
@@ -9,7 +10,7 @@ if _PIPELINE_ROOT not in sys.path:
|
|
|
9
10
|
|
|
10
11
|
from prizmkit_runtime.checkpoint_state import checkpoint_cursor as shared_checkpoint_cursor # noqa: E402
|
|
11
12
|
|
|
12
|
-
SUPPORTED_CONTINUATION_MODES = {"context_overflow"}
|
|
13
|
+
SUPPORTED_CONTINUATION_MODES = {"context_overflow", "ai_error"}
|
|
13
14
|
TASK_TYPES = {"feature", "bugfix", "refactor"}
|
|
14
15
|
|
|
15
16
|
|
|
@@ -19,13 +20,18 @@ def add_continuation_args(parser, default_task_type):
|
|
|
19
20
|
"--continuation-mode",
|
|
20
21
|
choices=sorted(SUPPORTED_CONTINUATION_MODES),
|
|
21
22
|
default="",
|
|
22
|
-
help="
|
|
23
|
+
help="Provider-neutral in-process AI error continuation mode.",
|
|
23
24
|
)
|
|
24
25
|
parser.add_argument(
|
|
25
26
|
"--previous-session-id",
|
|
26
27
|
default="",
|
|
27
28
|
help="Session ID that triggered this continuation.",
|
|
28
29
|
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--failure-classification",
|
|
32
|
+
default="context_overflow",
|
|
33
|
+
help="Classified outcome from the previous launched AI session.",
|
|
34
|
+
)
|
|
29
35
|
parser.add_argument(
|
|
30
36
|
"--continuation-count",
|
|
31
37
|
default="0",
|
|
@@ -36,6 +42,12 @@ def add_continuation_args(parser, default_task_type):
|
|
|
36
42
|
default="0",
|
|
37
43
|
help="Cumulative context overflow count for this task.",
|
|
38
44
|
)
|
|
45
|
+
parser.add_argument("--code-retry-count", default="0", help="Persisted code retry count.")
|
|
46
|
+
parser.add_argument("--infra-error-count", default="0", help="Persisted infrastructure retry count.")
|
|
47
|
+
parser.add_argument("--max-code-retries", default="0", help="Configured code retry budget.")
|
|
48
|
+
parser.add_argument("--max-infra-retries", default="0", help="Configured infrastructure retry budget.")
|
|
49
|
+
parser.add_argument("--no-progress-count", default="0", help="Consecutive context continuations without durable progress.")
|
|
50
|
+
parser.add_argument("--progress-fingerprint", default="", help="JSON durable Git/checkpoint/artifact progress summary.")
|
|
39
51
|
parser.add_argument(
|
|
40
52
|
"--task-type",
|
|
41
53
|
choices=sorted(TASK_TYPES),
|
|
@@ -72,6 +84,16 @@ def _safe_int(value, default=0):
|
|
|
72
84
|
return parsed if parsed >= 0 else default
|
|
73
85
|
|
|
74
86
|
|
|
87
|
+
def _safe_json_object(value):
|
|
88
|
+
if isinstance(value, dict):
|
|
89
|
+
return value
|
|
90
|
+
try:
|
|
91
|
+
parsed = json.loads(str(value or ""))
|
|
92
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
93
|
+
return {}
|
|
94
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
def _norm(path):
|
|
76
98
|
return str(path).replace(os.sep, "/")
|
|
77
99
|
|
|
@@ -193,7 +215,7 @@ def _build_summary(context):
|
|
|
193
215
|
"## Reason",
|
|
194
216
|
"",
|
|
195
217
|
"- Continuation mode: {}".format(context["mode"]),
|
|
196
|
-
"-
|
|
218
|
+
"- Previous AI session classification: {}".format(context["failure_classification"]),
|
|
197
219
|
"- This artifact is the durable handoff summary; do not rely on the previous full transcript.",
|
|
198
220
|
"",
|
|
199
221
|
"## Previous Session",
|
|
@@ -211,6 +233,17 @@ def _build_summary(context):
|
|
|
211
233
|
"- Base branch: {}".format(context["base_branch"] or "(unknown)"),
|
|
212
234
|
"- Continuation count: {}".format(context["continuation_count"]),
|
|
213
235
|
"- Context overflow count: {}".format(context["context_overflow_count"]),
|
|
236
|
+
"- Code retry count: {} / {}".format(context["code_retry_count"], context["max_code_retries"]),
|
|
237
|
+
"- Infrastructure retry count: {} / {}".format(context["infra_error_count"], context["max_infra_retries"]),
|
|
238
|
+
"- Consecutive no-progress context count: {}".format(context["no_progress_count"]),
|
|
239
|
+
"",
|
|
240
|
+
"## Git and Checkpoint Progress",
|
|
241
|
+
"",
|
|
242
|
+
"- Current checkpoint phase: {}".format(cursor["first_cursor"]),
|
|
243
|
+
"- Git status fingerprint: {}".format(context["progress_fingerprint"].get("git_diff_fingerprint", "(unknown)")),
|
|
244
|
+
"- HEAD commit: {}".format(context["progress_fingerprint"].get("head_commit", "(unknown)")),
|
|
245
|
+
"- Artifact fingerprint: {}".format(context["progress_fingerprint"].get("artifact_fingerprint", "(unknown)")),
|
|
246
|
+
"- Checkpoint cursor fingerprint: {}".format(context["progress_fingerprint"].get("checkpoint_cursor", "(unknown)")),
|
|
214
247
|
"",
|
|
215
248
|
"## Observed Partial Progress",
|
|
216
249
|
"",
|
|
@@ -247,16 +280,18 @@ def _build_addendum(context):
|
|
|
247
280
|
runtime = context["runtime"]
|
|
248
281
|
summary_paths = context["summary_paths"]
|
|
249
282
|
lines = [
|
|
250
|
-
"## Continuation Addendum —
|
|
283
|
+
"## Continuation Addendum — In-Process AI Error Recovery",
|
|
251
284
|
"",
|
|
252
|
-
"This is an automatic continuation after
|
|
285
|
+
"This is an automatic continuation after the previous launched AI session ended with `{}`.".format(context["failure_classification"]),
|
|
253
286
|
"This is a fresh headless AI CLI session for the same task.",
|
|
254
287
|
"Continue on the current branch/worktree.",
|
|
255
288
|
"Do not reset, clean, switch branches, or discard work.",
|
|
256
289
|
"",
|
|
257
290
|
"### Required First Actions",
|
|
258
291
|
"",
|
|
259
|
-
"-
|
|
292
|
+
"- Inspect `git status` and `git diff` before editing.",
|
|
293
|
+
"- Validate the workflow checkpoint and current phase before resuming.",
|
|
294
|
+
"- Inspect existing task artifacts and run only the tests necessary to establish current test state.",
|
|
260
295
|
"- Reuse the same durable task artifact directory: `{}`.".format(_norm(durable["artifact_dir"])),
|
|
261
296
|
"- Treat completed/skipped checkpoint steps as already done.",
|
|
262
297
|
"- Continue from the first pending/in-progress checkpoint step: `{}`.".format(cursor["first_cursor"]),
|
|
@@ -276,7 +311,10 @@ def _build_addendum(context):
|
|
|
276
311
|
"- Failure log path: `{}`".format(_norm(durable["failure_log"])),
|
|
277
312
|
"- Continuation summary path(s): `{}`".format(", ".join(_norm(p) for p in summary_paths)),
|
|
278
313
|
"- Continuation count: `{}`".format(context["continuation_count"]),
|
|
314
|
+
"- Previous failure classification: `{}`".format(context["failure_classification"]),
|
|
279
315
|
"- Context overflow count: `{}`".format(context["context_overflow_count"]),
|
|
316
|
+
"- Code retry count: `{}` / `{}`".format(context["code_retry_count"], context["max_code_retries"]),
|
|
317
|
+
"- Infrastructure retry count: `{}` / `{}`".format(context["infra_error_count"], context["max_infra_retries"]),
|
|
280
318
|
"- Previous session id: `{}`".format(context["previous_session_id"] or "(unknown)"),
|
|
281
319
|
"- Previous session log reference: `{}`".format(runtime["previous_session_log"]),
|
|
282
320
|
"",
|
|
@@ -316,8 +354,15 @@ def append_continuation_handoff(rendered_prompt, args, project_root, task_type,
|
|
|
316
354
|
"task_id": task_id,
|
|
317
355
|
"task_slug": task_slug,
|
|
318
356
|
"previous_session_id": previous_session_id,
|
|
357
|
+
"failure_classification": getattr(args, "failure_classification", "") or "context_overflow",
|
|
319
358
|
"continuation_count": _safe_int(getattr(args, "continuation_count", 0)),
|
|
320
359
|
"context_overflow_count": _safe_int(getattr(args, "context_overflow_count", 0)),
|
|
360
|
+
"code_retry_count": _safe_int(getattr(args, "code_retry_count", 0)),
|
|
361
|
+
"infra_error_count": _safe_int(getattr(args, "infra_error_count", 0)),
|
|
362
|
+
"max_code_retries": _safe_int(getattr(args, "max_code_retries", 0)),
|
|
363
|
+
"max_infra_retries": _safe_int(getattr(args, "max_infra_retries", 0)),
|
|
364
|
+
"no_progress_count": _safe_int(getattr(args, "no_progress_count", 0)),
|
|
365
|
+
"progress_fingerprint": _safe_json_object(getattr(args, "progress_fingerprint", "")),
|
|
321
366
|
"active_dev_branch": getattr(args, "active_dev_branch", ""),
|
|
322
367
|
"base_branch": getattr(args, "base_branch", ""),
|
|
323
368
|
"durable": durable,
|
|
@@ -276,6 +276,33 @@ def _set_continuation_common(status_data, args, session_id):
|
|
|
276
276
|
status_data["last_session_id"] = session_id
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
def _set_ai_error_continuation_metadata(status_data, args, session_id, reason):
|
|
280
|
+
"""Preserve durable task/session state for any retryable launched AI error."""
|
|
281
|
+
if args.active_dev_branch:
|
|
282
|
+
status_data["active_dev_branch"] = args.active_dev_branch
|
|
283
|
+
if args.base_branch:
|
|
284
|
+
status_data["base_branch"] = args.base_branch
|
|
285
|
+
status_data["continuation_pending"] = True
|
|
286
|
+
status_data["continuation_reason"] = reason
|
|
287
|
+
status_data["continuation_count"] = status_data.get("continuation_count", 0) + 1
|
|
288
|
+
status_data["previous_session_id"] = session_id
|
|
289
|
+
status_data["last_fatal_error_code"] = args.last_fatal_error_code or reason
|
|
290
|
+
if args.continuation_summary_path:
|
|
291
|
+
status_data["continuation_summary_path"] = args.continuation_summary_path
|
|
292
|
+
if args.no_progress_count is not None:
|
|
293
|
+
status_data["no_progress_count"] = max(0, args.no_progress_count)
|
|
294
|
+
fingerprint = _load_progress_fingerprint(args.progress_fingerprint)
|
|
295
|
+
if fingerprint is not None:
|
|
296
|
+
status_data["last_progress_fingerprint"] = fingerprint
|
|
297
|
+
status_data["resume_from_phase"] = None
|
|
298
|
+
if session_id:
|
|
299
|
+
sessions = status_data.get("sessions", [])
|
|
300
|
+
if session_id not in sessions:
|
|
301
|
+
sessions.append(session_id)
|
|
302
|
+
status_data["sessions"] = sessions
|
|
303
|
+
status_data["last_session_id"] = session_id
|
|
304
|
+
|
|
305
|
+
|
|
279
306
|
def set_context_overflow_continuation_metadata(status_data, args, session_id):
|
|
280
307
|
"""Record continuation metadata for a context-overflow outcome in status.json."""
|
|
281
308
|
status_data["context_overflow_count"] = status_data.get("context_overflow_count", 0) + 1
|
|
@@ -456,6 +483,18 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
456
483
|
|
|
457
484
|
# Track what status we write to bug-fix-list.json
|
|
458
485
|
new_status = current_list_status
|
|
486
|
+
if args.active_dev_branch:
|
|
487
|
+
bs["active_dev_branch"] = args.active_dev_branch
|
|
488
|
+
if args.base_branch:
|
|
489
|
+
bs["base_branch"] = args.base_branch
|
|
490
|
+
if args.last_fatal_error_code:
|
|
491
|
+
bs["last_fatal_error_code"] = args.last_fatal_error_code
|
|
492
|
+
if args.continuation_summary_path:
|
|
493
|
+
bs["continuation_summary_path"] = args.continuation_summary_path
|
|
494
|
+
fingerprint = _load_progress_fingerprint(args.progress_fingerprint)
|
|
495
|
+
if fingerprint is not None:
|
|
496
|
+
bs["last_progress_fingerprint"] = fingerprint
|
|
497
|
+
|
|
459
498
|
|
|
460
499
|
if session_status == "success":
|
|
461
500
|
bs["infra_error_count"] = 0
|
|
@@ -499,10 +538,14 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
499
538
|
|
|
500
539
|
if infra_error_count >= max_infra_retries:
|
|
501
540
|
new_status = "failed"
|
|
541
|
+
bs["continuation_pending"] = False
|
|
542
|
+
bs["continuation_reason"] = session_status
|
|
543
|
+
bs["needs_attention"] = True
|
|
502
544
|
if session_id:
|
|
503
545
|
bs["last_failed_session_id"] = session_id
|
|
504
546
|
else:
|
|
505
|
-
new_status = "
|
|
547
|
+
new_status = "in_progress"
|
|
548
|
+
_set_ai_error_continuation_metadata(bs, args, session_id, session_status)
|
|
506
549
|
|
|
507
550
|
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
508
551
|
if err:
|
|
@@ -528,27 +571,25 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
528
571
|
else:
|
|
529
572
|
bs["retry_count"] = bs.get("retry_count", 0) + 1
|
|
530
573
|
|
|
531
|
-
cleaned = cleanup_bug_artifacts(
|
|
532
|
-
state_dir=state_dir,
|
|
533
|
-
bug_id=bug_id,
|
|
534
|
-
project_root=args.project_root,
|
|
535
|
-
)
|
|
536
|
-
|
|
537
574
|
if bs["retry_count"] >= max_retries:
|
|
538
575
|
new_status = "failed"
|
|
576
|
+
bs["continuation_pending"] = False
|
|
577
|
+
bs["continuation_reason"] = session_status
|
|
578
|
+
bs["needs_attention"] = True
|
|
539
579
|
else:
|
|
540
|
-
new_status = "
|
|
580
|
+
new_status = "in_progress"
|
|
581
|
+
_set_ai_error_continuation_metadata(bs, args, session_id, session_status)
|
|
541
582
|
|
|
542
583
|
bs["resume_from_phase"] = None
|
|
543
|
-
|
|
544
|
-
|
|
584
|
+
if session_id:
|
|
585
|
+
bs["last_failed_session_id"] = session_id
|
|
545
586
|
|
|
546
587
|
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
547
588
|
if err:
|
|
548
589
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
549
590
|
return
|
|
550
591
|
|
|
551
|
-
if
|
|
592
|
+
if session_id:
|
|
552
593
|
sessions = bs.get("sessions", [])
|
|
553
594
|
if session_id not in sessions:
|
|
554
595
|
sessions.append(session_id)
|
|
@@ -581,8 +622,9 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
581
622
|
summary["degraded_reason"] = session_status
|
|
582
623
|
summary["restart_policy"] = "finalization_retry"
|
|
583
624
|
elif session_status == "infra_error":
|
|
584
|
-
summary["restart_policy"] = "
|
|
625
|
+
summary["restart_policy"] = "in_process_ai_error_continuation" if new_status == "in_progress" else "needs_attention"
|
|
585
626
|
summary["infra_error_count"] = bs.get("infra_error_count", 0)
|
|
627
|
+
summary.update(continuation_metadata_summary(bs))
|
|
586
628
|
summary["artifacts_preserved"] = True
|
|
587
629
|
elif session_status == "context_overflow":
|
|
588
630
|
summary["restart_policy"] = "context_overflow_continuation"
|
|
@@ -593,8 +635,9 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
593
635
|
summary.update(continuation_metadata_summary(bs))
|
|
594
636
|
summary["artifacts_preserved"] = True
|
|
595
637
|
elif session_status != "success":
|
|
596
|
-
summary["restart_policy"] = "
|
|
597
|
-
summary
|
|
638
|
+
summary["restart_policy"] = "in_process_ai_error_continuation" if new_status == "in_progress" else "needs_attention"
|
|
639
|
+
summary.update(continuation_metadata_summary(bs))
|
|
640
|
+
summary["artifacts_preserved"] = True
|
|
598
641
|
|
|
599
642
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
600
643
|
|