prizmkit 1.1.98 → 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 (79) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/README.md +5 -5
  3. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +1 -1
  4. package/bundled/dev-pipeline/launch-feature-daemon.sh +1 -1
  5. package/bundled/dev-pipeline/launch-refactor-daemon.sh +1 -1
  6. package/bundled/dev-pipeline/lib/common.sh +244 -25
  7. package/bundled/dev-pipeline/reset-bug.sh +32 -2
  8. package/bundled/dev-pipeline/reset-feature.sh +32 -2
  9. package/bundled/dev-pipeline/reset-refactor.sh +32 -2
  10. package/bundled/dev-pipeline/run-bugfix.sh +133 -18
  11. package/bundled/dev-pipeline/run-feature.sh +134 -18
  12. package/bundled/dev-pipeline/run-refactor.sh +133 -18
  13. package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
  14. package/bundled/dev-pipeline/scripts/continuation.py +374 -0
  15. package/bundled/dev-pipeline/scripts/detect-stuck.py +5 -5
  16. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +29 -35
  17. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +19 -25
  18. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +19 -25
  19. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +1 -1
  20. package/bundled/dev-pipeline/scripts/init-pipeline.py +1 -1
  21. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +1 -1
  22. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +146 -16
  23. package/bundled/dev-pipeline/scripts/update-bug-status.py +220 -13
  24. package/bundled/dev-pipeline/scripts/update-feature-status.py +244 -18
  25. package/bundled/dev-pipeline/scripts/update-refactor-status.py +220 -13
  26. package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +1 -1
  27. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +2 -2
  28. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +3 -3
  29. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +3 -3
  30. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
  31. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
  32. package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +31 -66
  33. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  34. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  35. package/bundled/dev-pipeline/tests/test_auto_skip.py +518 -8
  36. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +103 -0
  37. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +60 -0
  38. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
  39. package/bundled/dev-pipeline-windows/lib/common.ps1 +172 -10
  40. package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +95 -11
  41. package/bundled/dev-pipeline-windows/lib/reset.ps1 +34 -0
  42. package/bundled/dev-pipeline-windows/reset-bug.ps1 +1 -0
  43. package/bundled/dev-pipeline-windows/reset-feature.ps1 +1 -0
  44. package/bundled/dev-pipeline-windows/reset-refactor.ps1 +1 -0
  45. package/bundled/dev-pipeline-windows/scripts/check-session-status.py +74 -1
  46. package/bundled/dev-pipeline-windows/scripts/continuation.py +374 -0
  47. package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +5 -5
  48. package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +29 -35
  49. package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +20 -25
  50. package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +20 -25
  51. package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +1 -1
  52. package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +1 -1
  53. package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +1 -1
  54. package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +146 -16
  55. package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +220 -13
  56. package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +244 -18
  57. package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +220 -13
  58. package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +1 -1
  59. package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +2 -2
  60. package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +3 -3
  61. package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +3 -3
  62. package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +1 -1
  63. package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +3 -3
  64. package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +31 -68
  65. package/bundled/dev-pipeline-windows/templates/session-status-schema.json +1 -1
  66. package/bundled/skills/_metadata.json +1 -1
  67. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +2 -2
  68. package/bundled/skills/feature-pipeline-launcher/SKILL.md +2 -2
  69. package/bundled/skills/recovery-workflow/SKILL.md +2 -2
  70. package/bundled/skills/recovery-workflow/evals/evals.json +2 -2
  71. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +2 -2
  72. package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +2 -2
  73. package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +2 -2
  74. package/bundled/skills-windows/recovery-workflow/SKILL.md +2 -2
  75. package/bundled/skills-windows/recovery-workflow/evals/evals.json +2 -2
  76. package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +2 -2
  77. package/package.json +1 -1
  78. package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
  79. 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-bugfix-prompt")
30
30
 
@@ -48,6 +48,8 @@ def parse_args():
48
48
  parser.add_argument("--mode", default=None, help="Pipeline execution mode override: lite, standard, full")
49
49
  parser.add_argument("--critic", default=None, help="Enable critic agent: true/false")
50
50
  parser.add_argument("--no-checkpoint", action="store_true", help="Do not write workflow-checkpoint.json (used by pipeline dry-run)")
51
+ add_continuation_args(parser, "bugfix")
52
+
51
53
  return parser.parse_args()
52
54
 
53
55
 
@@ -221,7 +223,7 @@ def get_prev_session_status(state_dir, bug_id):
221
223
  if not state_dir:
222
224
  return "N/A (first run)"
223
225
 
224
- bug_status_path = os.path.join(state_dir, "bugs", bug_id, "status.json")
226
+ bug_status_path = os.path.join(state_dir, bug_id, "status.json")
225
227
  if not os.path.isfile(bug_status_path):
226
228
  return "N/A (first run)"
227
229
 
@@ -235,8 +237,7 @@ def get_prev_session_status(state_dir, bug_id):
235
237
  if not last_session_id:
236
238
  return "N/A (first run)"
237
239
 
238
- session_status_path = os.path.join(
239
- state_dir, "bugs", bug_id, "sessions",
240
+ session_status_path = os.path.join(state_dir, bug_id, "sessions",
240
241
  last_session_id, "session-status.json"
241
242
  )
242
243
  if not os.path.isfile(session_status_path):
@@ -307,7 +308,7 @@ def build_replacements(args, bug, global_context, script_dir):
307
308
 
308
309
  # Session status path
309
310
  session_status_path = os.path.join(
310
- project_root, ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
311
+ project_root, ".prizmkit", "state", "bugfix", args.bug_id,
311
312
  "sessions", args.session_id, "session-status.json"
312
313
  )
313
314
 
@@ -359,33 +360,15 @@ def build_replacements(args, bug, global_context, script_dir):
359
360
  browser_verify_steps = " # (reproduce bug and verify fix)"
360
361
 
361
362
  max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
362
- if max_log_size == 0:
363
- poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
364
- else:
365
- poll_interval = _parse_positive_int_env(
366
- "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
367
- )
368
363
 
369
364
  replacements = {
370
365
  "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
371
366
  "{{MAX_LOG_SIZE}}": str(max_log_size),
372
367
  "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
373
- "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
374
- "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
375
- ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
376
- "sessions", args.session_id, "logs", "log-monitor-status.txt",
377
- ),
378
- "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
379
- ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
380
- "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
381
- ),
382
368
  "{{SESSION_LOG_PATH}}": os.path.join(
383
- ".prizmkit", "state", "bugfix", "bugs", args.bug_id,
369
+ ".prizmkit", "state", "bugfix", args.bug_id,
384
370
  "sessions", args.session_id, "logs", "session.log",
385
371
  ),
386
- "{{LOG_MONITOR_SCRIPT}}": os.path.join(
387
- ".prizmkit", "dev-pipeline", "scripts", "monitor-log.ps1",
388
- ),
389
372
  "{{RUN_ID}}": args.run_id,
390
373
  "{{SESSION_ID}}": args.session_id,
391
374
  "{{BUG_ID}}": args.bug_id,
@@ -663,6 +646,8 @@ def main():
663
646
 
664
647
  # Build replacements
665
648
  replacements = build_replacements(args, bug, global_context, script_dir)
649
+ if is_continuation(args):
650
+ replacements["{{LOG_SIZE_AWARENESS}}"] = ""
666
651
 
667
652
  # Add checkpoint path to replacements
668
653
  checkpoint_rel = os.path.join(
@@ -681,6 +666,7 @@ def main():
681
666
  # Generate checkpoint file
682
667
  project_root = resolve_project_root(script_dir)
683
668
  checkpoint_path = os.path.join(project_root, checkpoint_rel)
669
+ checkpoint = {}
684
670
  if not args.no_checkpoint:
685
671
  checkpoint_dir = os.path.dirname(checkpoint_path)
686
672
  os.makedirs(checkpoint_dir, exist_ok=True)
@@ -688,7 +674,7 @@ def main():
688
674
  checkpoint = generate_bugfix_checkpoint(args.bug_id, args.session_id)
689
675
 
690
676
  is_resume = args.resume_phase != "null"
691
- if is_resume and os.path.exists(checkpoint_path):
677
+ if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
692
678
  try:
693
679
  with open(checkpoint_path, "r", encoding="utf-8") as f:
694
680
  existing = json.load(f)
@@ -707,6 +693,15 @@ def main():
707
693
  json.dump(checkpoint, f, indent=2, ensure_ascii=False)
708
694
  LOGGER.info("Wrote bugfix checkpoint to %s", checkpoint_path)
709
695
 
696
+ if is_continuation(args):
697
+ rendered = append_continuation_handoff(
698
+ rendered, args, project_root, args.task_type, args.bug_id,
699
+ args.bug_id, checkpoint,
700
+ )
701
+ err = write_output(args.output, rendered)
702
+ if err:
703
+ emit_failure(err)
704
+
710
705
  # Resolve critic and mode
711
706
  bug_critic = bug.get("critic", False)
712
707
  if args.critic is not None:
@@ -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
 
@@ -139,6 +139,8 @@ def parse_args():
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
141
  parser.add_argument("--no-checkpoint", action="store_true", help="Do not write workflow-checkpoint.json (used by pipeline dry-run)")
142
+ add_continuation_args(parser, "refactor")
143
+
142
144
  return parser.parse_args()
143
145
 
144
146
 
@@ -376,7 +378,7 @@ def get_prev_session_status(state_dir, refactor_id):
376
378
  if not state_dir:
377
379
  return "N/A (first run)"
378
380
 
379
- refactor_status_path = os.path.join(state_dir, "refactors", refactor_id, "status.json")
381
+ refactor_status_path = os.path.join(state_dir, refactor_id, "status.json")
380
382
  try:
381
383
  with open(refactor_status_path, "r", encoding="utf-8") as f:
382
384
  refactor_status = json.load(f)
@@ -387,8 +389,7 @@ def get_prev_session_status(state_dir, refactor_id):
387
389
  if not last_session_id:
388
390
  return "N/A (first run)"
389
391
 
390
- session_status_path = os.path.join(
391
- state_dir, "refactors", refactor_id, "sessions",
392
+ session_status_path = os.path.join(state_dir, refactor_id, "sessions",
392
393
  last_session_id, "session-status.json"
393
394
  )
394
395
  try:
@@ -456,7 +457,7 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
456
457
 
457
458
  # Session status path
458
459
  session_status_path = os.path.join(
459
- project_root, ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
460
+ project_root, ".prizmkit", "state", "refactor", args.refactor_id,
460
461
  "sessions", args.session_id, "session-status.json"
461
462
  )
462
463
 
@@ -519,33 +520,15 @@ def build_replacements(args, refactor, refactors, global_context, script_dir):
519
520
  browser_verify_steps = " # (validate UI renders correctly and feature still works)"
520
521
 
521
522
  max_log_size = _parse_non_negative_int_env("MAX_LOG_SIZE", DEFAULT_MAX_LOG_SIZE)
522
- if max_log_size == 0:
523
- poll_interval = DEFAULT_LOG_MONITOR_POLL_INTERVAL
524
- else:
525
- poll_interval = _parse_positive_int_env(
526
- "LOG_MONITOR_POLL_INTERVAL", DEFAULT_LOG_MONITOR_POLL_INTERVAL,
527
- )
528
523
 
529
524
  replacements = {
530
525
  "{{LOG_SIZE_AWARENESS}}": load_log_size_section(script_dir),
531
526
  "{{MAX_LOG_SIZE}}": str(max_log_size),
532
527
  "{{MAX_LOG_SIZE_HUMAN}}": _format_bytes(max_log_size),
533
- "{{LOG_MONITOR_POLL_INTERVAL}}": str(poll_interval),
534
- "{{LOG_MONITOR_STATUS_FILE}}": os.path.join(
535
- ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
536
- "sessions", args.session_id, "logs", "log-monitor-status.txt",
537
- ),
538
- "{{LOG_MONITOR_BASELINE_FILE}}": os.path.join(
539
- ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
540
- "sessions", args.session_id, "logs", "log-monitor-baseline.txt",
541
- ),
542
528
  "{{SESSION_LOG_PATH}}": os.path.join(
543
- ".prizmkit", "state", "refactor", "refactors", args.refactor_id,
529
+ ".prizmkit", "state", "refactor", args.refactor_id,
544
530
  "sessions", args.session_id, "logs", "session.log",
545
531
  ),
546
- "{{LOG_MONITOR_SCRIPT}}": os.path.join(
547
- ".prizmkit", "dev-pipeline", "scripts", "monitor-log.ps1",
548
- ),
549
532
  "{{RUN_ID}}": args.run_id,
550
533
  "{{SESSION_ID}}": args.session_id,
551
534
  "{{REFACTOR_ID}}": args.refactor_id,
@@ -745,6 +728,8 @@ def main():
745
728
 
746
729
  # Build replacements
747
730
  replacements = build_replacements(args, refactor, refactors, global_context, script_dir)
731
+ if is_continuation(args):
732
+ replacements["{{LOG_SIZE_AWARENESS}}"] = ""
748
733
 
749
734
  # Render the template
750
735
  rendered = render_template(template_content, replacements, args.resume_phase, refactor)
@@ -760,6 +745,7 @@ def main():
760
745
  ".prizmkit", "refactor", args.refactor_id, "workflow-checkpoint.json",
761
746
  )
762
747
  checkpoint_path = os.path.join(project_root, checkpoint_rel)
748
+ checkpoint = {}
763
749
  if not args.no_checkpoint:
764
750
  checkpoint_dir = os.path.dirname(checkpoint_path)
765
751
  os.makedirs(checkpoint_dir, exist_ok=True)
@@ -767,7 +753,7 @@ def main():
767
753
  checkpoint = generate_refactor_checkpoint(args.refactor_id, args.session_id)
768
754
 
769
755
  is_resume = args.resume_phase != "null"
770
- if is_resume and os.path.exists(checkpoint_path):
756
+ if (is_resume or is_continuation(args)) and os.path.exists(checkpoint_path):
771
757
  try:
772
758
  with open(checkpoint_path, "r", encoding="utf-8") as f:
773
759
  existing = json.load(f)
@@ -786,6 +772,15 @@ def main():
786
772
  json.dump(checkpoint, f, indent=2, ensure_ascii=False)
787
773
  LOGGER.info("Wrote refactor checkpoint to %s", checkpoint_path)
788
774
 
775
+ if is_continuation(args):
776
+ rendered = append_continuation_handoff(
777
+ rendered, args, project_root, args.task_type, args.refactor_id,
778
+ args.refactor_id, checkpoint,
779
+ )
780
+ err = write_output(args.output, rendered)
781
+ if err:
782
+ emit_failure(err)
783
+
789
784
  # Resolve critic and mode
790
785
  refactor_critic = refactor.get("critic", False)
791
786
  if args.critic is not None:
@@ -207,7 +207,7 @@ def create_state_directory(state_dir, bug_list_path, bugs):
207
207
  abs_bug_list_path = os.path.abspath(bug_list_path)
208
208
  # Store as relative path from state_dir so pipeline.json is portable across machines
209
209
  rel_bug_list_path = os.path.relpath(abs_bug_list_path, abs_state_dir)
210
- bugs_dir = os.path.join(abs_state_dir, "bugs")
210
+ bugs_dir = abs_state_dir
211
211
 
212
212
  now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
213
213
  run_id = "bugfix-run-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
@@ -242,7 +242,7 @@ def create_state_directory(state_dir, feature_list_path, features):
242
242
  abs_feature_list_path = os.path.abspath(feature_list_path)
243
243
  # Store as relative path from state_dir so pipeline.json is portable across machines
244
244
  rel_feature_list_path = os.path.relpath(abs_feature_list_path, abs_state_dir)
245
- features_dir = os.path.join(abs_state_dir, "features")
245
+ features_dir = abs_state_dir
246
246
 
247
247
  now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
248
248
  run_id = "run-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
@@ -283,7 +283,7 @@ def create_state_directory(state_dir, refactor_list_path, refactors):
283
283
  abs_refactor_list_path = os.path.abspath(refactor_list_path)
284
284
  # Store as relative path from state_dir so pipeline.json is portable across machines
285
285
  rel_refactor_list_path = os.path.relpath(abs_refactor_list_path, abs_state_dir)
286
- refactors_dir = os.path.join(abs_state_dir, "refactors")
286
+ refactors_dir = abs_state_dir
287
287
 
288
288
  now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
289
289
  run_id = "refactor-run-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
@@ -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)