prizmkit 1.1.99 → 1.1.100
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 +244 -25
- package/bundled/dev-pipeline/reset-bug.sh +30 -0
- package/bundled/dev-pipeline/reset-feature.sh +30 -0
- package/bundled/dev-pipeline/reset-refactor.sh +30 -0
- package/bundled/dev-pipeline/run-bugfix.sh +128 -13
- package/bundled/dev-pipeline/run-feature.sh +130 -14
- package/bundled/dev-pipeline/run-refactor.sh +128 -13
- package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +27 -30
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
- package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +510 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
- package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +93 -9
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +27 -30
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +16 -20
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +16 -20
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
- package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
- package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
|
@@ -20,11 +20,11 @@ import re
|
|
|
20
20
|
import sys
|
|
21
21
|
|
|
22
22
|
from utils import enrich_global_context, load_json_file, read_platform_conventions, setup_logging
|
|
23
|
+
from continuation import add_continuation_args, append_continuation_handoff, is_continuation
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
DEFAULT_MAX_RETRIES = 3
|
|
26
27
|
DEFAULT_MAX_LOG_SIZE = 2097152
|
|
27
|
-
DEFAULT_LOG_MONITOR_POLL_INTERVAL = 30
|
|
28
28
|
|
|
29
29
|
LOGGER = setup_logging("generate-refactor-prompt")
|
|
30
30
|
|
|
@@ -138,6 +138,8 @@ def parse_args():
|
|
|
138
138
|
parser.add_argument("--template", default=None, help="Custom template path. Defaults to {script_dir}/../templates/refactor-bootstrap-prompt.md")
|
|
139
139
|
parser.add_argument("--mode", default=None, help="Pipeline execution mode override: lite, standard, full")
|
|
140
140
|
parser.add_argument("--critic", default=None, help="Enable critic agent: true/false")
|
|
141
|
+
add_continuation_args(parser, "refactor")
|
|
142
|
+
|
|
141
143
|
return parser.parse_args()
|
|
142
144
|
|
|
143
145
|
|
|
@@ -517,33 +519,15 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
|
|
|
517
519
|
browser_verify_steps = " # (validate UI renders correctly and feature still works)"
|
|
518
520
|
|
|
519
521
|
max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
|
|
520
|
-
if max_log_size == 0:
|
|
521
|
-
poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
|
|
522
|
-
else:
|
|
523
|
-
poll_interval = _parse_positive_int_env(
|
|
524
|
-
"LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
|
|
525
|
-
)
|
|
526
522
|
|
|
527
523
|
replacements = {
|
|
528
524
|
"{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
|
|
529
525
|
"{{MAX_LOG_SIZE}}": str(max_log_size),
|
|
530
526
|
"{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
|
|
531
|
-
"{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
|
|
532
|
-
"{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
|
|
533
|
-
".prizmkit", "state", "refactor", args.refactor_id,
|
|
534
|
-
"sessions", args.session_id, "logs", "log-monitor-status.txt",
|
|
535
|
-
),
|
|
536
|
-
"{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
|
|
537
|
-
".prizmkit", "state", "refactor", args.refactor_id,
|
|
538
|
-
"sessions", args.session_id, "logs", "log-monitor-baseline.txt",
|
|
539
|
-
),
|
|
540
527
|
"{{SESSION_LOG_PATH}}": os.path.join(
|
|
541
528
|
".prizmkit", "state", "refactor", args.refactor_id,
|
|
542
529
|
"sessions", args.session_id, "logs", "session.log",
|
|
543
530
|
),
|
|
544
|
-
"{{LOG_MONITOR_SCRIPT}}": os.path.join(
|
|
545
|
-
".prizmkit", "dev-pipeline", "scripts", "monitor-log.sh",
|
|
546
|
-
),
|
|
547
531
|
"{{RUN_ID}}": args.run_id,
|
|
548
532
|
"{{SESSION_ID}}": args.session_id,
|
|
549
533
|
"{{REFACTOR_ID}}": args.refactor_id,
|
|
@@ -742,6 +726,8 @@ def main():
|
|
|
742
726
|
|
|
743
727
|
# Build replacements
|
|
744
728
|
replacements = build_replacements(args, refactor, refactors, global_context, script_dir)
|
|
729
|
+
if is_continuation(args):
|
|
730
|
+
replacements["{{LOG_SIZE_AWARENESS}}"] = ""
|
|
745
731
|
|
|
746
732
|
# Render the template
|
|
747
733
|
rendered = render_template(template_content, replacements, args.resume_phase, refactor)
|
|
@@ -763,7 +749,7 @@ def main():
|
|
|
763
749
|
checkpoint = generate_refactor_checkpoint(args.refactor_id, args.session_id)
|
|
764
750
|
|
|
765
751
|
is_resume = args.resume_phase != "null"
|
|
766
|
-
if is_resume and os.path.exists(checkpoint_path):
|
|
752
|
+
if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
|
|
767
753
|
try:
|
|
768
754
|
with open(checkpoint_path, "r", encoding="utf-8") as f:
|
|
769
755
|
existing = json.load(f)
|
|
@@ -782,6 +768,15 @@ def main():
|
|
|
782
768
|
json.dump(checkpoint, f, indent=2, ensure_ascii=False)
|
|
783
769
|
LOGGER.info("Wrote refactor checkpoint to %s", checkpoint_path)
|
|
784
770
|
|
|
771
|
+
if is_continuation(args):
|
|
772
|
+
rendered = append_continuation_handoff(
|
|
773
|
+
rendered, args, project_root, args.task_type, args.refactor_id,
|
|
774
|
+
args.refactor_id, checkpoint,
|
|
775
|
+
)
|
|
776
|
+
err = write_output(args.output, rendered)
|
|
777
|
+
if err:
|
|
778
|
+
emit_failure(err)
|
|
779
|
+
|
|
785
780
|
# Resolve critic and mode
|
|
786
781
|
refactor_critic = refactor.get("critic", False)
|
|
787
782
|
if args.critic is not None:
|
|
@@ -60,30 +60,66 @@ PHASE_KEYWORDS = {
|
|
|
60
60
|
},
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
CONTEXT_OVERFLOW_CODE = "context_overflow"
|
|
64
|
+
|
|
65
|
+
# Exact machine/wrapper signals. These are only treated as self-sufficient when
|
|
66
|
+
# they appear in structured error-code fields or as explicit wrapper markers.
|
|
67
|
+
CONTEXT_CODE_PATTERNS = [
|
|
68
|
+
re.compile(pattern, re.IGNORECASE)
|
|
69
|
+
for pattern in (
|
|
70
|
+
r"\bcontext_overflow\b",
|
|
71
|
+
r"\bcontext_too_large\b",
|
|
72
|
+
r"\bcontext_length_exceeded\b",
|
|
73
|
+
r"\bmodel_context_window_exceeded\b",
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
WRAPPER_CONTEXT_ERROR_PATTERNS = [
|
|
78
|
+
re.compile(pattern, re.IGNORECASE)
|
|
79
|
+
for pattern in (
|
|
80
|
+
r"\bPRIZMKIT_FATAL_ERROR\s*=\s*context_overflow\b",
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# Natural-language provider messages are weak signals: they require an adjacent
|
|
85
|
+
# runtime/API error context unless a structured field already proves the result
|
|
86
|
+
# is an error.
|
|
87
|
+
WEAK_CONTEXT_ERROR_PATTERNS = [
|
|
64
88
|
re.compile(pattern, re.IGNORECASE)
|
|
65
89
|
for pattern in (
|
|
66
|
-
r"context_too_large",
|
|
67
|
-
r"model_context_window_exceeded",
|
|
68
90
|
r"Your input exceeds the context window",
|
|
69
|
-
r"input exceeds the context window",
|
|
91
|
+
r"input exceeds (?:the )?(?:model )?context window",
|
|
70
92
|
r"context window of this model",
|
|
71
|
-
r"context window exceeded",
|
|
72
|
-
r"
|
|
73
|
-
r"context
|
|
93
|
+
r"context window (?:was )?exceeded",
|
|
94
|
+
r"exceeded (?:the )?context window",
|
|
95
|
+
r"maximum context length exceeded",
|
|
96
|
+
r"prompt is too long",
|
|
97
|
+
r"too many tokens",
|
|
98
|
+
r"input token count exceeds",
|
|
99
|
+
r"input tokens? exceeds?",
|
|
100
|
+
r"token count exceeds",
|
|
74
101
|
)
|
|
75
102
|
]
|
|
76
103
|
|
|
104
|
+
REQUEST_TOO_LARGE_PATTERN = re.compile(r"\brequest (?:is )?too large\b", re.IGNORECASE)
|
|
105
|
+
REQUEST_TOO_LARGE_SEMANTIC_PATTERN = re.compile(
|
|
106
|
+
r"\b(context|token|tokens|prompt|input|model)\b",
|
|
107
|
+
re.IGNORECASE,
|
|
108
|
+
)
|
|
109
|
+
|
|
77
110
|
ERROR_CONTEXT_PATTERNS = [
|
|
78
111
|
re.compile(pattern, re.IGNORECASE)
|
|
79
112
|
for pattern in (
|
|
80
113
|
r"\bapi error\b",
|
|
81
114
|
r"invalid_request_error",
|
|
115
|
+
r"\bbad request\b",
|
|
82
116
|
r"\bstatus\s*[:=]?\s*(400|413)\b",
|
|
83
117
|
r"\bapi_error_status\b",
|
|
84
118
|
r"\bapi_error_code\b",
|
|
85
119
|
r"\blast_result_is_error\b\s*[\"':=]*\s*true\b",
|
|
86
120
|
r"\bis_error\b\s*[\"':=]*\s*true\b",
|
|
121
|
+
r"\berror\s*[:=]",
|
|
122
|
+
r"\bfatal\s*[:=]",
|
|
87
123
|
)
|
|
88
124
|
]
|
|
89
125
|
|
|
@@ -95,21 +131,41 @@ def _has_error_context(text):
|
|
|
95
131
|
return any(pattern.search(text) for pattern in ERROR_CONTEXT_PATTERNS)
|
|
96
132
|
|
|
97
133
|
|
|
98
|
-
def
|
|
134
|
+
def _has_context_overflow_signal(text, structured_code=False):
|
|
135
|
+
"""Return true when text contains a context-window/token overflow signal."""
|
|
136
|
+
if not text:
|
|
137
|
+
return False
|
|
138
|
+
if any(pattern.search(text) for pattern in WRAPPER_CONTEXT_ERROR_PATTERNS):
|
|
139
|
+
return True
|
|
140
|
+
if structured_code and any(pattern.search(text) for pattern in CONTEXT_CODE_PATTERNS):
|
|
141
|
+
return True
|
|
142
|
+
if any(pattern.search(text) for pattern in CONTEXT_CODE_PATTERNS):
|
|
143
|
+
return True
|
|
144
|
+
if any(pattern.search(text) for pattern in WEAK_CONTEXT_ERROR_PATTERNS):
|
|
145
|
+
return True
|
|
146
|
+
if REQUEST_TOO_LARGE_PATTERN.search(text) and REQUEST_TOO_LARGE_SEMANTIC_PATTERN.search(text):
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def detect_api_error_code(text, require_error_context=False, structured_code=False):
|
|
99
152
|
"""Return a normalized fatal/runtime error code from terminal text.
|
|
100
153
|
|
|
101
|
-
Structured
|
|
102
|
-
directly.
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
Structured error-code fields and explicit wrapper markers can be matched
|
|
155
|
+
directly. Natural-language provider messages require runtime/API context to
|
|
156
|
+
avoid false positives from prompts, tests, or documentation that merely
|
|
157
|
+
mention context windows or token limits.
|
|
105
158
|
"""
|
|
106
159
|
if not text:
|
|
107
160
|
return ""
|
|
161
|
+
if any(pattern.search(text) for pattern in WRAPPER_CONTEXT_ERROR_PATTERNS):
|
|
162
|
+
return CONTEXT_OVERFLOW_CODE
|
|
163
|
+
if structured_code and any(pattern.search(text) for pattern in CONTEXT_CODE_PATTERNS):
|
|
164
|
+
return CONTEXT_OVERFLOW_CODE
|
|
108
165
|
if require_error_context and not _has_error_context(text):
|
|
109
166
|
return ""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return "context_too_large"
|
|
167
|
+
if _has_context_overflow_signal(text, structured_code=structured_code):
|
|
168
|
+
return CONTEXT_OVERFLOW_CODE
|
|
113
169
|
return ""
|
|
114
170
|
|
|
115
171
|
|
|
@@ -171,6 +227,7 @@ class ProgressTracker:
|
|
|
171
227
|
with sessionId/cwd metadata, function_call for tool invocations).
|
|
172
228
|
"""
|
|
173
229
|
event_type = event.get("type", "")
|
|
230
|
+
self._detect_structured_terminal_fields(event)
|
|
174
231
|
|
|
175
232
|
# ── Codex exec --json JSONL format ──────────────────────────
|
|
176
233
|
if event_type in (
|
|
@@ -491,6 +548,7 @@ class ProgressTracker:
|
|
|
491
548
|
is_error=bool(event.get("is_error")),
|
|
492
549
|
api_error_status=event.get("api_error_status"),
|
|
493
550
|
api_error_code=str(api_error_code or ""),
|
|
551
|
+
stop_reason=str(event.get("stop_reason") or ""),
|
|
494
552
|
)
|
|
495
553
|
|
|
496
554
|
# ── Claude API raw stream format ────────────────────────────
|
|
@@ -579,6 +637,78 @@ class ProgressTracker:
|
|
|
579
637
|
# This is a sub-agent event; tool name is still tracked normally
|
|
580
638
|
pass
|
|
581
639
|
|
|
640
|
+
|
|
641
|
+
def _detect_structured_terminal_fields(self, event):
|
|
642
|
+
"""Detect provider-neutral terminal/context errors before format routing."""
|
|
643
|
+
if not isinstance(event, dict):
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
code_parts = []
|
|
647
|
+
text_parts = []
|
|
648
|
+
terminal_parts = []
|
|
649
|
+
|
|
650
|
+
for key in ("fatal_error_code", "api_error_code", "error_code", "stop_reason"):
|
|
651
|
+
value = event.get(key)
|
|
652
|
+
if isinstance(value, (str, int, float)) and str(value).strip():
|
|
653
|
+
code_parts.append(str(value))
|
|
654
|
+
|
|
655
|
+
for key in ("terminal_result_text", "message", "result"):
|
|
656
|
+
value = event.get(key)
|
|
657
|
+
if isinstance(value, (str, int, float)) and str(value).strip():
|
|
658
|
+
text_parts.append(str(value))
|
|
659
|
+
terminal_parts.append(str(value))
|
|
660
|
+
|
|
661
|
+
error_obj = event.get("error")
|
|
662
|
+
if isinstance(error_obj, dict):
|
|
663
|
+
for key in ("type", "code", "message"):
|
|
664
|
+
value = error_obj.get(key)
|
|
665
|
+
if isinstance(value, (str, int, float)) and str(value).strip():
|
|
666
|
+
text_parts.append(str(value))
|
|
667
|
+
terminal_parts.append(str(value))
|
|
668
|
+
elif isinstance(error_obj, (str, int, float)) and str(error_obj).strip():
|
|
669
|
+
text_parts.append(str(error_obj))
|
|
670
|
+
terminal_parts.append(str(error_obj))
|
|
671
|
+
|
|
672
|
+
combined_code_text = " ".join(code_parts)
|
|
673
|
+
combined_text = " ".join(code_parts + text_parts)
|
|
674
|
+
if not combined_text:
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
error_like = (
|
|
678
|
+
bool(code_parts)
|
|
679
|
+
or event.get("api_error_status") not in (None, "")
|
|
680
|
+
or bool(event.get("last_result_is_error"))
|
|
681
|
+
or bool(event.get("is_error"))
|
|
682
|
+
or _has_error_context(combined_text)
|
|
683
|
+
)
|
|
684
|
+
normalized_code = detect_api_error_code(
|
|
685
|
+
combined_code_text,
|
|
686
|
+
require_error_context=False,
|
|
687
|
+
structured_code=True,
|
|
688
|
+
) or detect_api_error_code(
|
|
689
|
+
combined_text,
|
|
690
|
+
require_error_context=not error_like,
|
|
691
|
+
structured_code=bool(code_parts),
|
|
692
|
+
)
|
|
693
|
+
if not normalized_code:
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
self.last_result_is_error = True
|
|
697
|
+
api_error_status = event.get("api_error_status") or event.get("status")
|
|
698
|
+
if api_error_status not in (None, ""):
|
|
699
|
+
try:
|
|
700
|
+
self.api_error_status = int(api_error_status)
|
|
701
|
+
except (TypeError, ValueError):
|
|
702
|
+
self.api_error_status = api_error_status
|
|
703
|
+
self.api_error_code = normalized_code
|
|
704
|
+
self.fatal_error_code = normalized_code
|
|
705
|
+
terminal_text = " ".join(terminal_parts) or combined_text
|
|
706
|
+
self.terminal_result_text = terminal_text[:1000]
|
|
707
|
+
if terminal_text.strip():
|
|
708
|
+
self.last_text_snippet = terminal_text.strip()[:120]
|
|
709
|
+
if not self.errors or self.errors[-1] != normalized_code:
|
|
710
|
+
self.errors.append(normalized_code)
|
|
711
|
+
|
|
582
712
|
def _record_cb_agent_tool_call(self, tool_name, raw_args):
|
|
583
713
|
"""Record a CodeBuddy Agent/Task* tool invocation for sub-agent tracking.
|
|
584
714
|
|
|
@@ -616,7 +746,7 @@ class ProgressTracker:
|
|
|
616
746
|
elif tool_name == "TaskCreate":
|
|
617
747
|
self._subagent_spawn_count += 1
|
|
618
748
|
|
|
619
|
-
def _record_terminal_result(self, text="", is_error=False, api_error_status=None, api_error_code=""):
|
|
749
|
+
def _record_terminal_result(self, text="", is_error=False, api_error_status=None, api_error_code="", stop_reason=""):
|
|
620
750
|
"""Record a Claude Code terminal result event."""
|
|
621
751
|
terminal_text = str(text or "")
|
|
622
752
|
self.last_result_is_error = bool(is_error)
|
|
@@ -42,6 +42,8 @@ SESSION_STATUS_VALUES = [
|
|
|
42
42
|
"crashed",
|
|
43
43
|
"timed_out",
|
|
44
44
|
"infra_error",
|
|
45
|
+
"context_overflow",
|
|
46
|
+
"stalled_context_continuation",
|
|
45
47
|
"commit_missing",
|
|
46
48
|
"docs_missing",
|
|
47
49
|
"merge_conflict",
|
|
@@ -78,6 +80,12 @@ def parse_args():
|
|
|
78
80
|
parser.add_argument("--max-retries", type=int, default=3, help="Maximum code retry count (default: 3)")
|
|
79
81
|
parser.add_argument("--max-infra-retries", type=int, default=3, help="Maximum infrastructure retry count (default: 3)")
|
|
80
82
|
parser.add_argument("--project-root", default=None, help="Project root directory. Required for 'clean' action.")
|
|
83
|
+
parser.add_argument("--active-dev-branch", default=None, help="Development branch preserved for continuation after context overflow.")
|
|
84
|
+
parser.add_argument("--base-branch", default=None, help="Original/base branch for continuation branch reuse.")
|
|
85
|
+
parser.add_argument("--last-fatal-error-code", default=None, help="Fatal runtime error code associated with continuation.")
|
|
86
|
+
parser.add_argument("--continuation-summary-path", default=None, help="Optional continuation summary artifact path.")
|
|
87
|
+
parser.add_argument("--no-progress-count", type=int, default=None, help="Consecutive context-overflow sessions without objective progress.")
|
|
88
|
+
parser.add_argument("--progress-fingerprint", default=None, help="JSON progress fingerprint captured after the session.")
|
|
81
89
|
return parser.parse_args()
|
|
82
90
|
|
|
83
91
|
|
|
@@ -164,6 +172,128 @@ def update_bug_in_list(bug_list_path, bug_id, new_status):
|
|
|
164
172
|
return write_json_file(bug_list_path, data)
|
|
165
173
|
|
|
166
174
|
|
|
175
|
+
CONTINUATION_PENDING_FIELDS = [
|
|
176
|
+
"active_dev_branch",
|
|
177
|
+
"base_branch",
|
|
178
|
+
"continuation_pending",
|
|
179
|
+
"continuation_reason",
|
|
180
|
+
"last_context_overflow_session_id",
|
|
181
|
+
"previous_session_id",
|
|
182
|
+
"last_fatal_error_code",
|
|
183
|
+
"continuation_summary_path",
|
|
184
|
+
"no_progress_count",
|
|
185
|
+
"last_progress_fingerprint",
|
|
186
|
+
"needs_attention",
|
|
187
|
+
"stalled_context_continuation",
|
|
188
|
+
"stalled_reason",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
CONTINUATION_COUNTER_FIELDS = [
|
|
192
|
+
"context_overflow_count",
|
|
193
|
+
"continuation_count",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def has_pending_continuation_metadata(status_data):
|
|
198
|
+
"""Return True when runtime state still has pending continuation metadata."""
|
|
199
|
+
return any(key in status_data for key in CONTINUATION_PENDING_FIELDS)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def clear_pending_continuation_metadata(status_data):
|
|
203
|
+
"""Clear branch/pending continuation fields while preserving observability counters."""
|
|
204
|
+
for key in CONTINUATION_PENDING_FIELDS:
|
|
205
|
+
status_data.pop(key, None)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def reset_continuation_metadata(status_data):
|
|
209
|
+
"""Clear all continuation runtime metadata, including manual-reset counters."""
|
|
210
|
+
clear_pending_continuation_metadata(status_data)
|
|
211
|
+
for key in CONTINUATION_COUNTER_FIELDS:
|
|
212
|
+
status_data.pop(key, None)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _load_progress_fingerprint(raw):
|
|
219
|
+
if not raw:
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
return json.loads(raw)
|
|
223
|
+
except (TypeError, ValueError):
|
|
224
|
+
return {"unparsed": str(raw)}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _set_continuation_common(status_data, args, session_id):
|
|
228
|
+
if args.active_dev_branch:
|
|
229
|
+
status_data["active_dev_branch"] = args.active_dev_branch
|
|
230
|
+
if args.base_branch:
|
|
231
|
+
status_data["base_branch"] = args.base_branch
|
|
232
|
+
status_data["last_context_overflow_session_id"] = session_id
|
|
233
|
+
status_data["previous_session_id"] = session_id
|
|
234
|
+
status_data["last_fatal_error_code"] = args.last_fatal_error_code or "context_overflow"
|
|
235
|
+
if args.continuation_summary_path:
|
|
236
|
+
status_data["continuation_summary_path"] = args.continuation_summary_path
|
|
237
|
+
else:
|
|
238
|
+
status_data.pop("continuation_summary_path", None)
|
|
239
|
+
if args.no_progress_count is not None:
|
|
240
|
+
status_data["no_progress_count"] = max(0, args.no_progress_count)
|
|
241
|
+
fingerprint = _load_progress_fingerprint(args.progress_fingerprint)
|
|
242
|
+
if fingerprint is not None:
|
|
243
|
+
status_data["last_progress_fingerprint"] = fingerprint
|
|
244
|
+
status_data["last_infra_error_session_id"] = None
|
|
245
|
+
status_data["resume_from_phase"] = None
|
|
246
|
+
if session_id:
|
|
247
|
+
status_data["last_session_id"] = session_id
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def set_context_overflow_continuation_metadata(status_data, args, session_id):
|
|
251
|
+
"""Record continuation metadata for a context-overflow outcome in status.json."""
|
|
252
|
+
status_data["context_overflow_count"] = status_data.get("context_overflow_count", 0) + 1
|
|
253
|
+
status_data["continuation_count"] = status_data.get("continuation_count", 0) + 1
|
|
254
|
+
status_data["continuation_pending"] = True
|
|
255
|
+
status_data["continuation_reason"] = "context_overflow"
|
|
256
|
+
status_data.pop("needs_attention", None)
|
|
257
|
+
status_data.pop("stalled_context_continuation", None)
|
|
258
|
+
status_data.pop("stalled_reason", None)
|
|
259
|
+
_set_continuation_common(status_data, args, session_id)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def set_stalled_context_continuation_metadata(status_data, args, session_id):
|
|
263
|
+
"""Record no-progress context-overflow stall without consuming retry budgets."""
|
|
264
|
+
status_data["context_overflow_count"] = status_data.get("context_overflow_count", 0) + 1
|
|
265
|
+
status_data["continuation_count"] = status_data.get("continuation_count", 0) + 1
|
|
266
|
+
_set_continuation_common(status_data, args, session_id)
|
|
267
|
+
status_data["continuation_pending"] = False
|
|
268
|
+
status_data["continuation_reason"] = STALL_REASON
|
|
269
|
+
status_data["needs_attention"] = True
|
|
270
|
+
status_data["stalled_context_continuation"] = True
|
|
271
|
+
status_data["stalled_reason"] = STALL_REASON
|
|
272
|
+
if args.no_progress_count is None or args.no_progress_count < 2:
|
|
273
|
+
status_data["no_progress_count"] = 2
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def continuation_metadata_summary(status_data):
|
|
277
|
+
"""Return additive continuation fields for CLI JSON outputs."""
|
|
278
|
+
return {
|
|
279
|
+
"active_dev_branch": status_data.get("active_dev_branch"),
|
|
280
|
+
"base_branch": status_data.get("base_branch"),
|
|
281
|
+
"continuation_pending": status_data.get("continuation_pending", False),
|
|
282
|
+
"continuation_reason": status_data.get("continuation_reason"),
|
|
283
|
+
"last_context_overflow_session_id": status_data.get("last_context_overflow_session_id"),
|
|
284
|
+
"last_fatal_error_code": status_data.get("last_fatal_error_code"),
|
|
285
|
+
"previous_session_id": status_data.get("previous_session_id"),
|
|
286
|
+
"context_overflow_count": status_data.get("context_overflow_count", 0),
|
|
287
|
+
"continuation_count": status_data.get("continuation_count", 0),
|
|
288
|
+
"no_progress_count": status_data.get("no_progress_count", 0),
|
|
289
|
+
"last_progress_fingerprint": status_data.get("last_progress_fingerprint"),
|
|
290
|
+
"needs_attention": status_data.get("needs_attention", False),
|
|
291
|
+
"stalled_context_continuation": status_data.get("stalled_context_continuation", False),
|
|
292
|
+
"stalled_reason": status_data.get("stalled_reason"),
|
|
293
|
+
"continuation_summary_path": status_data.get("continuation_summary_path"),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
167
297
|
# ---------------------------------------------------------------------------
|
|
168
298
|
# Action: get_next
|
|
169
299
|
# ---------------------------------------------------------------------------
|
|
@@ -244,6 +374,7 @@ def action_get_next(bug_list_data, state_dir):
|
|
|
244
374
|
"infra_error_count": chosen_status_data.get("infra_error_count", 0),
|
|
245
375
|
"resume_from_phase": chosen_status_data.get("resume_from_phase", None),
|
|
246
376
|
}
|
|
377
|
+
result.update(continuation_metadata_summary(chosen_status_data))
|
|
247
378
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
248
379
|
|
|
249
380
|
|
|
@@ -267,12 +398,15 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
267
398
|
|
|
268
399
|
bs = load_bug_status(state_dir, bug_id)
|
|
269
400
|
|
|
401
|
+
current_list_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
402
|
+
|
|
270
403
|
# Track what status we write to bug-fix-list.json
|
|
271
|
-
new_status =
|
|
404
|
+
new_status = current_list_status
|
|
272
405
|
|
|
273
406
|
if session_status == "success":
|
|
274
407
|
bs["infra_error_count"] = 0
|
|
275
408
|
bs["last_infra_error_session_id"] = None
|
|
409
|
+
clear_pending_continuation_metadata(bs)
|
|
276
410
|
new_status = "completed"
|
|
277
411
|
bs["resume_from_phase"] = None
|
|
278
412
|
err = update_bug_in_list(bug_list_path, bug_id, "completed")
|
|
@@ -304,7 +438,7 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
304
438
|
bs["infra_error_count"] = infra_error_count
|
|
305
439
|
bs["last_infra_error_session_id"] = session_id
|
|
306
440
|
bs["max_infra_retries"] = max_infra_retries
|
|
307
|
-
bs["degraded_reason"] =
|
|
441
|
+
bs["degraded_reason"] = session_status
|
|
308
442
|
if session_id:
|
|
309
443
|
bs["last_session_id"] = session_id
|
|
310
444
|
bs["resume_from_phase"] = None
|
|
@@ -316,6 +450,23 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
316
450
|
else:
|
|
317
451
|
new_status = "pending"
|
|
318
452
|
|
|
453
|
+
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
454
|
+
if err:
|
|
455
|
+
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
456
|
+
return
|
|
457
|
+
elif session_status == "context_overflow":
|
|
458
|
+
# Context overflow is continuation state, not code or infra retry state.
|
|
459
|
+
set_context_overflow_continuation_metadata(bs, args, session_id)
|
|
460
|
+
new_status = "in_progress"
|
|
461
|
+
if current_list_status != "in_progress":
|
|
462
|
+
err = update_bug_in_list(bug_list_path, bug_id, "in_progress")
|
|
463
|
+
if err:
|
|
464
|
+
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
465
|
+
return
|
|
466
|
+
elif session_status == "stalled_context_continuation":
|
|
467
|
+
# No-progress context-overflow stall pauses automation without consuming retry budgets.
|
|
468
|
+
set_stalled_context_continuation_metadata(bs, args, session_id)
|
|
469
|
+
new_status = "needs_info"
|
|
319
470
|
err = update_bug_in_list(bug_list_path, bug_id, new_status)
|
|
320
471
|
if err:
|
|
321
472
|
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
@@ -375,6 +526,14 @@ def action_update(args, bug_list_path, state_dir):
|
|
|
375
526
|
summary["restart_policy"] = "infra_retry"
|
|
376
527
|
summary["infra_error_count"] = bs.get("infra_error_count", 0)
|
|
377
528
|
summary["artifacts_preserved"] = True
|
|
529
|
+
elif session_status == "context_overflow":
|
|
530
|
+
summary["restart_policy"] = "context_overflow_continuation"
|
|
531
|
+
summary.update(continuation_metadata_summary(bs))
|
|
532
|
+
summary["artifacts_preserved"] = True
|
|
533
|
+
elif session_status == "stalled_context_continuation":
|
|
534
|
+
summary["restart_policy"] = "needs_attention"
|
|
535
|
+
summary.update(continuation_metadata_summary(bs))
|
|
536
|
+
summary["artifacts_preserved"] = True
|
|
378
537
|
elif session_status != "success":
|
|
379
538
|
summary["restart_policy"] = "full_restart"
|
|
380
539
|
summary["cleanup_performed"] = cleaned
|
|
@@ -445,6 +604,45 @@ def cleanup_bug_artifacts(state_dir, bug_id, project_root=None):
|
|
|
445
604
|
return cleaned
|
|
446
605
|
|
|
447
606
|
|
|
607
|
+
def _resolve_project_file(project_root, path):
|
|
608
|
+
if not path:
|
|
609
|
+
return None
|
|
610
|
+
if os.path.isabs(path):
|
|
611
|
+
target = os.path.abspath(path)
|
|
612
|
+
else:
|
|
613
|
+
target = os.path.abspath(os.path.join(project_root, path))
|
|
614
|
+
root = os.path.abspath(project_root)
|
|
615
|
+
try:
|
|
616
|
+
rel = os.path.relpath(target, root)
|
|
617
|
+
except ValueError:
|
|
618
|
+
return None
|
|
619
|
+
if rel == os.pardir or rel.startswith(os.pardir + os.sep):
|
|
620
|
+
return None
|
|
621
|
+
return target
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def cleanup_continuation_artifacts(project_root, status_data, candidate_paths=None):
|
|
625
|
+
"""Delete durable continuation handoff files for a manual clean reset."""
|
|
626
|
+
candidates = []
|
|
627
|
+
if isinstance(status_data, dict):
|
|
628
|
+
candidates.append(status_data.get("continuation_summary_path"))
|
|
629
|
+
candidates.extend(candidate_paths or [])
|
|
630
|
+
|
|
631
|
+
cleaned = []
|
|
632
|
+
seen = set()
|
|
633
|
+
for candidate in candidates:
|
|
634
|
+
target = _resolve_project_file(project_root, candidate)
|
|
635
|
+
if not target or target in seen:
|
|
636
|
+
continue
|
|
637
|
+
seen.add(target)
|
|
638
|
+
if os.path.basename(target) != "continuation-summary.md":
|
|
639
|
+
continue
|
|
640
|
+
if os.path.isfile(target):
|
|
641
|
+
os.remove(target)
|
|
642
|
+
cleaned.append("Deleted continuation artifact {}".format(target))
|
|
643
|
+
return cleaned
|
|
644
|
+
|
|
645
|
+
|
|
448
646
|
def load_session_status(state_dir, bug_id, session_id):
|
|
449
647
|
session_status_path = os.path.join(state_dir, bug_id, "sessions",
|
|
450
648
|
session_id, "session-status.json"
|
|
@@ -608,6 +806,7 @@ def action_reset(args, bug_list_path, state_dir):
|
|
|
608
806
|
bs["sessions"] = []
|
|
609
807
|
bs["last_session_id"] = None
|
|
610
808
|
bs["resume_from_phase"] = None
|
|
809
|
+
reset_continuation_metadata(bs)
|
|
611
810
|
bs["updated_at"] = now_iso()
|
|
612
811
|
|
|
613
812
|
err = save_bug_status(state_dir, bug_id, bs)
|
|
@@ -679,12 +878,19 @@ def action_clean(args, bug_list_path, state_dir):
|
|
|
679
878
|
old_status = get_bug_status_from_list(bug_list_path, bug_id)
|
|
680
879
|
old_retry = bs.get("retry_count", 0)
|
|
681
880
|
|
|
881
|
+
continuation_candidates = [
|
|
882
|
+
os.path.join(".prizmkit", "bugfix", bug_id, "continuation-summary.md"),
|
|
883
|
+
os.path.join(".prizmkit", "specs", bug_id, "continuation-summary.md"),
|
|
884
|
+
]
|
|
885
|
+
cleaned.extend(cleanup_continuation_artifacts(project_root, bs, continuation_candidates))
|
|
886
|
+
|
|
682
887
|
bs["retry_count"] = 0
|
|
683
888
|
bs["infra_error_count"] = 0
|
|
684
889
|
bs["last_infra_error_session_id"] = None
|
|
685
890
|
bs["sessions"] = []
|
|
686
891
|
bs["last_session_id"] = None
|
|
687
892
|
bs["resume_from_phase"] = None
|
|
893
|
+
reset_continuation_metadata(bs)
|
|
688
894
|
bs["updated_at"] = now_iso()
|
|
689
895
|
|
|
690
896
|
err = save_bug_status(state_dir, bug_id, bs)
|
|
@@ -760,10 +966,11 @@ def action_start(args, bug_list_path, state_dir):
|
|
|
760
966
|
error_out("Failed to save bug status: {}".format(err))
|
|
761
967
|
return
|
|
762
968
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
969
|
+
if old_status != "in_progress":
|
|
970
|
+
err = update_bug_in_list(bug_list_path, bug_id, "in_progress")
|
|
971
|
+
if err:
|
|
972
|
+
error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
|
|
973
|
+
return
|
|
767
974
|
|
|
768
975
|
result = {
|
|
769
976
|
"action": "start",
|
|
@@ -853,6 +1060,7 @@ def action_unskip(args, bug_list_path, state_dir):
|
|
|
853
1060
|
bs["sessions"] = []
|
|
854
1061
|
bs["last_session_id"] = None
|
|
855
1062
|
bs["resume_from_phase"] = None
|
|
1063
|
+
reset_continuation_metadata(bs)
|
|
856
1064
|
bs["updated_at"] = now_iso()
|
|
857
1065
|
save_bug_status(state_dir, bid, bs)
|
|
858
1066
|
|