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.
Files changed (57) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/lib/common.sh +244 -25
  3. package/bundled/dev-pipeline/reset-bug.sh +30 -0
  4. package/bundled/dev-pipeline/reset-feature.sh +30 -0
  5. package/bundled/dev-pipeline/reset-refactor.sh +30 -0
  6. package/bundled/dev-pipeline/run-bugfix.sh +128 -13
  7. package/bundled/dev-pipeline/run-feature.sh +130 -14
  8. package/bundled/dev-pipeline/run-refactor.sh +128 -13
  9. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  10. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  11. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +27 -30
  12. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +15 -20
  13. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
  14. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
  15. package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
  16. package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
  17. package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
  18. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  19. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
  20. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
  21. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
  22. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  23. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  24. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
  25. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  26. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  27. package/bundled/dev-pipeline/tests/test_auto_skip.py +510 -0
  28. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
  29. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
  30. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  31. package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
  32. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +93 -9
  33. package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
  34. package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
  35. package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
  36. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
  37. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
  38. package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
  39. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +27 -30
  40. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +16 -20
  41. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +16 -20
  42. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
  43. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +214 -6
  44. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +237 -6
  45. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +214 -6
  46. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
  47. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
  48. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
  49. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
  50. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
  51. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
  52. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
  53. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
  54. package/bundled/skills/_metadata.json +1 -1
  55. package/package.json +1 -1
  56. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  57. 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
- CONTEXT_ERROR_PATTERNS = [
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"invalid_request_error.*context window",
73
- r"context window.*invalid_request_error",
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 detect_api_error_code(text, require_error_context=False):
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 terminal result/error events and raw stderr can be matched
102
- directly. Ordinary assistant prose is noisier: it may mention the phrase
103
- "input exceeds the context window" while explaining a test or recovery
104
- rule, so callers can require additional error-like context there.
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
- for pattern in CONTEXT_ERROR_PATTERNS:
111
- if pattern.search(text):
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 = get_bug_status_from_list(bug_list_path, bug_id)
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"] = "infra_error"
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
- err = update_bug_in_list(bug_list_path, bug_id, "in_progress")
764
- if err:
765
- error_out("Failed to update .prizmkit/plans/bug-fix-list.json: {}".format(err))
766
- return
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