jleechanorg-pr-automation 0.2.41__tar.gz → 0.2.82__tar.gz

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 (65) hide show
  1. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/PKG-INFO +4 -3
  2. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/README.md +1 -1
  3. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/__init__.py +15 -4
  4. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_safety_manager.py +104 -27
  5. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/codex_config.py +2 -0
  6. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1347 -346
  7. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/logging_utils.py +5 -3
  8. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +145 -0
  9. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
  10. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/orchestrated_pr_runner.py +835 -0
  11. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_atomic_race_condition.py +261 -0
  12. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +25 -16
  13. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_auto_detect_user.py +90 -0
  14. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +2 -1
  15. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_safety_limits.py +7 -3
  16. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +2 -2
  17. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cleanup_pending_reviews.py +550 -0
  18. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cli_agent_argument.py +125 -0
  19. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_comment_validation.py +227 -0
  20. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cursor_bot_round2_bugs.py +263 -0
  21. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_cursor_bug_fixes.py +274 -0
  22. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_dirty_repo_handling.py +195 -0
  23. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_fixpr_prompt.py +9 -5
  24. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_fixpr_return_value.py +506 -0
  25. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_inflight_cache_reload.py +191 -0
  26. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_jleechanorg_pr_monitor_requests.py +326 -0
  27. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_model_parameter.py +23 -17
  28. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +1203 -0
  29. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +2 -1
  30. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +1013 -0
  31. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_pr_targeting.py +1 -1
  32. jleechanorg_pr_automation-0.2.82/jleechanorg_pr_automation/tests/test_rolling_window.py +377 -0
  33. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +25 -2
  34. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/utils.py +60 -1
  35. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/PKG-INFO +4 -3
  36. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/SOURCES.txt +12 -0
  37. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/requires.txt +1 -0
  38. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/pyproject.toml +5 -4
  39. jleechanorg_pr_automation-0.2.41/jleechanorg_pr_automation/orchestrated_pr_runner.py +0 -497
  40. jleechanorg_pr_automation-0.2.41/jleechanorg_pr_automation/tests/test_fixpr_return_value.py +0 -140
  41. jleechanorg_pr_automation-0.2.41/jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +0 -697
  42. jleechanorg_pr_automation-0.2.41/jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +0 -354
  43. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +0 -0
  44. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_safety_wrapper.py +0 -0
  45. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/automation_utils.py +0 -0
  46. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/check_codex_comment.py +0 -0
  47. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/codex_branch_updater.py +0 -0
  48. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/__init__.py +0 -0
  49. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/debug_page_content.py +0 -0
  50. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/oracle_cli.py +0 -0
  51. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +0 -0
  52. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/openai_automation/test_codex_integration.py +0 -0
  53. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/__init__.py +0 -0
  54. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/conftest.py +0 -0
  55. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +0 -0
  56. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_automation_marker_functions.py +0 -0
  57. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_codex_actor_matching.py +0 -0
  58. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_graphql_error_handling.py +0 -0
  59. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_packaging_integration.py +0 -0
  60. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_version_consistency.py +0 -0
  61. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +0 -0
  62. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/dependency_links.txt +0 -0
  63. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/entry_points.txt +0 -0
  64. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/jleechanorg_pr_automation.egg-info/top_level.txt +0 -0
  65. {jleechanorg_pr_automation-0.2.41 → jleechanorg_pr_automation-0.2.82}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jleechanorg-pr-automation
3
- Version: 0.2.41
3
+ Version: 0.2.82
4
4
  Summary: GitHub PR automation system with safety limits and actionable counting
5
5
  Author-email: jleechan <jlee@jleechan.org>
6
6
  License-Expression: MIT
@@ -17,13 +17,14 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Classifier: Topic :: Software Development :: Version Control :: Git
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.11
21
21
  Description-Content-Type: text/markdown
22
22
  Requires-Dist: requests>=2.25.0
23
23
  Requires-Dist: jleechanorg-orchestration>=0.1.18
24
24
  Requires-Dist: playwright>=1.40.0
25
25
  Requires-Dist: playwright-stealth>=1.0.0
26
26
  Requires-Dist: aiohttp>=3.8.0
27
+ Requires-Dist: PyYAML>=6.0
27
28
  Provides-Extra: email
28
29
  Requires-Dist: keyring>=23.0.0; extra == "email"
29
30
  Provides-Extra: dev
@@ -290,7 +291,7 @@ jleechanorg-pr-monitor --fixpr --dry-run
290
291
 
291
292
  ```bash
292
293
  # Monitor and fix in one command
293
- jleechanorg-pr-monitor --fixpr --max-prs 5 --fixpr-agent claude
294
+ jleechanorg-pr-monitor --fixpr --max-prs 5 --cli-agent claude
294
295
  ```
295
296
 
296
297
  ### Agent CLI Options
@@ -255,7 +255,7 @@ jleechanorg-pr-monitor --fixpr --dry-run
255
255
 
256
256
  ```bash
257
257
  # Monitor and fix in one command
258
- jleechanorg-pr-monitor --fixpr --max-prs 5 --fixpr-agent claude
258
+ jleechanorg-pr-monitor --fixpr --max-prs 5 --cli-agent claude
259
259
  ```
260
260
 
261
261
  ### Agent CLI Options
@@ -6,12 +6,23 @@ safety features, intelligent filtering, and cross-process synchronization.
6
6
  """
7
7
 
8
8
  import re
9
- from importlib.metadata import PackageNotFoundError, version as dist_version
9
+ from importlib.metadata import PackageNotFoundError
10
+ from importlib.metadata import version as dist_version
10
11
  from pathlib import Path
11
- from typing import Optional
12
+ from typing import Any
12
13
 
13
14
  from .automation_safety_manager import AutomationSafetyManager
14
- from .jleechanorg_pr_monitor import JleechanorgPRMonitor
15
+
16
+
17
+ # Lazy import to avoid RuntimeWarning when running as script
18
+ # Use __getattr__ for Python 3.7+ lazy module imports
19
+ def __getattr__(name: str) -> Any:
20
+ """Lazy import of JleechanorgPRMonitor to avoid frozen module warning."""
21
+ if name == "JleechanorgPRMonitor":
22
+ from .jleechanorg_pr_monitor import JleechanorgPRMonitor
23
+ return JleechanorgPRMonitor
24
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
25
+
15
26
  from .utils import (
16
27
  SafeJSONManager,
17
28
  get_automation_limits,
@@ -26,7 +37,7 @@ _SECTION_RE = re.compile(r"^\s*\[[^\]]+\]\s*$")
26
37
  _VERSION_RE = re.compile(r'^\s*version\s*=\s*"([^"]+)"\s*$')
27
38
 
28
39
 
29
- def _version_from_pyproject(pyproject_path: Path) -> Optional[str]:
40
+ def _version_from_pyproject(pyproject_path: Path) -> str | None:
30
41
  if not pyproject_path.exists():
31
42
  return None
32
43
 
@@ -17,7 +17,7 @@ import os
17
17
  import smtplib
18
18
  import sys
19
19
  import threading
20
- from datetime import datetime, timedelta
20
+ from datetime import datetime, timedelta, timezone
21
21
  from email.mime.multipart import MIMEMultipart
22
22
  from email.mime.text import MIMEText
23
23
  from typing import Dict, Optional, Union
@@ -194,6 +194,29 @@ class AutomationSafetyManager:
194
194
  # Sync inflight cache to prevent concurrent processing
195
195
  self._write_json_file(self.inflight_file, self._pr_inflight_cache)
196
196
 
197
+ def _parse_timestamp(self, timestamp_str: str) -> datetime:
198
+ """Parse ISO timestamp string to datetime object.
199
+
200
+ Returns:
201
+ datetime object in UTC, or epoch (1970-01-01) if parsing fails
202
+ """
203
+ if not timestamp_str:
204
+ return datetime(1970, 1, 1, tzinfo=timezone.utc)
205
+
206
+ try:
207
+ # Parse ISO format timestamp (e.g., "2026-01-18T02:33:26.798956+00:00")
208
+ dt = datetime.fromisoformat(timestamp_str)
209
+ # Ensure timezone-aware (convert to UTC if needed)
210
+ if dt.tzinfo is None:
211
+ dt = dt.replace(tzinfo=timezone.utc)
212
+ return dt
213
+ except (ValueError, AttributeError, TypeError):
214
+ # Return epoch if parse fails (very old attempt, will be filtered out)
215
+ # TypeError: timestamp is not a string (e.g., int from corrupted/legacy data)
216
+ # ValueError: timestamp string is malformed
217
+ # AttributeError: timestamp_str is None or has no expected attributes
218
+ return datetime(1970, 1, 1, tzinfo=timezone.utc)
219
+
197
220
  def _make_pr_key(
198
221
  self,
199
222
  pr_number: Union[int, str],
@@ -354,11 +377,12 @@ class AutomationSafetyManager:
354
377
  def can_process_pr(self, pr_number: Union[int, str], repo: str = None, branch: str = None) -> bool:
355
378
  """Check if PR can be processed (under attempt limit).
356
379
 
357
- NEW BEHAVIOR: Counts ALL attempts (success + failure) against the limit.
380
+ NEW BEHAVIOR: Uses rolling window (default 24 hours) instead of daily reset.
381
+ Attempts expire gradually as they age out of the window, not all at midnight.
358
382
  Supports per-PR limit overrides (0 = unlimited).
359
383
 
360
384
  Blocks if:
361
- 1. Total attempts >= effective_pr_limit (respects overrides)
385
+ 1. Attempts in last N hours >= effective_pr_limit (respects overrides)
362
386
  """
363
387
  with self.lock:
364
388
  raw_data = self._read_json_file(self.pr_attempts_file)
@@ -370,54 +394,106 @@ class AutomationSafetyManager:
370
394
  # Get effective limit (checks for per-PR override)
371
395
  effective_limit = self._get_effective_pr_limit(pr_number, repo, branch)
372
396
 
373
- # Count ALL attempts (not just failures)
374
- total_attempts = len(attempts)
397
+ # Filter attempts to rolling window (last N hours)
398
+ # Get window hours from env var or config (default 24)
399
+ # Use coerce_positive_int to gracefully handle invalid env var values
400
+ window_hours = coerce_positive_int(
401
+ os.environ.get("AUTOMATION_ATTEMPT_WINDOW_HOURS", "24"),
402
+ default=24
403
+ )
404
+ cutoff_time = datetime.now(timezone.utc) - timedelta(hours=window_hours)
405
+
406
+ window_attempts = [
407
+ attempt for attempt in attempts
408
+ if isinstance(attempt, dict) and self._parse_timestamp(attempt.get("timestamp")) >= cutoff_time
409
+ ]
410
+
411
+ # Count attempts within rolling window
412
+ total_attempts = len(window_attempts)
375
413
 
376
414
  # Check against effective limit
377
415
  return total_attempts < effective_limit
378
416
 
379
417
  def try_process_pr(self, pr_number: Union[int, str], repo: str = None, branch: str = None) -> bool:
380
- """Atomically reserve a processing slot for PR."""
418
+ """Atomically reserve a processing slot for PR with proper cross-process locking."""
381
419
  with self.lock:
382
420
  # Check consecutive failure limit first
383
421
  if not self.can_process_pr(pr_number, repo, branch):
384
422
  return False
385
423
 
386
424
  pr_key = self._make_pr_key(pr_number, repo, branch)
387
- inflight = self._pr_inflight_cache.get(pr_key, 0)
388
425
 
389
- # Check if we're at the concurrent processing limit for this PR
390
- if inflight >= self.pr_limit:
391
- return False
426
+ # Use atomic_update to prevent race conditions between processes
427
+ # This holds the file lock across the entire read-modify-write operation
428
+ success = False
392
429
 
393
- # Reserve a processing slot
394
- self._pr_inflight_cache[pr_key] = inflight + 1
430
+ def reserve_slot(inflight_data: dict) -> dict:
431
+ nonlocal success
432
+ # Update in-memory cache from disk data
433
+ self._pr_inflight_cache = {k: int(v) for k, v in inflight_data.items()}
395
434
 
396
- # Persist immediately to prevent race conditions with concurrent cron jobs
397
- self._write_json_file(self.inflight_file, self._pr_inflight_cache)
435
+ inflight = self._pr_inflight_cache.get(pr_key, 0)
398
436
 
399
- return True
437
+ # Check if we're at the concurrent processing limit for this PR
438
+ if inflight >= self.pr_limit:
439
+ success = False
440
+ return inflight_data # Return unchanged
441
+
442
+ # Reserve a processing slot
443
+ self._pr_inflight_cache[pr_key] = inflight + 1
444
+ success = True
445
+ return self._pr_inflight_cache
446
+
447
+ json_manager.atomic_update(self.inflight_file, reserve_slot, {})
448
+ return success
400
449
 
401
450
  def release_pr_slot(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
402
- """Release a processing slot for PR (call in finally block)"""
451
+ """Release a processing slot for PR (call in finally block) with atomic cross-process locking."""
403
452
  with self.lock:
404
453
  pr_key = self._make_pr_key(pr_number, repo, branch)
405
- inflight = self._pr_inflight_cache.get(pr_key, 0)
406
- if inflight > 0:
407
- self._pr_inflight_cache[pr_key] = inflight - 1
408
- # Persist immediately to prevent race conditions
409
- self._write_json_file(self.inflight_file, self._pr_inflight_cache)
454
+
455
+ # Use atomic_update to prevent race conditions between processes
456
+ def release_slot(inflight_data: dict) -> dict:
457
+ # Update in-memory cache from disk data
458
+ self._pr_inflight_cache = {k: int(v) for k, v in inflight_data.items()}
459
+
460
+ inflight = self._pr_inflight_cache.get(pr_key, 0)
461
+ if inflight > 0:
462
+ self._pr_inflight_cache[pr_key] = inflight - 1
463
+ if self._pr_inflight_cache[pr_key] == 0:
464
+ self._pr_inflight_cache.pop(pr_key, None)
465
+
466
+ return self._pr_inflight_cache
467
+
468
+ json_manager.atomic_update(self.inflight_file, release_slot, {})
410
469
 
411
470
  def get_pr_attempts(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
412
- """Get count of ALL attempts (success + failure) for a specific PR.
471
+ """Get count of attempts for a specific PR.
413
472
 
414
- NEW BEHAVIOR: Counts ALL attempts, not just consecutive failures.
473
+ NEW BEHAVIOR: Uses rolling window (consistent with can_process_pr()).
474
+ This ensures CLI output matches actual attempt counting logic.
415
475
  """
416
476
  with self.lock:
477
+ raw_data = self._read_json_file(self.pr_attempts_file)
478
+ self._pr_attempts_cache = self._normalize_pr_attempt_keys(raw_data)
417
479
  pr_key = self._make_pr_key(pr_number, repo, branch)
418
480
  attempts = list(self._pr_attempts_cache.get(pr_key, []))
419
- # Return total count of ALL attempts
420
- return len(attempts)
481
+
482
+ # Filter attempts to rolling window (last N hours)
483
+ # MUST match the logic in can_process_pr() to avoid misleading CLI output
484
+ window_hours = coerce_positive_int(
485
+ os.environ.get("AUTOMATION_ATTEMPT_WINDOW_HOURS", "24"),
486
+ default=24
487
+ )
488
+ cutoff_time = datetime.now(timezone.utc) - timedelta(hours=window_hours)
489
+
490
+ window_attempts = [
491
+ attempt for attempt in attempts
492
+ if isinstance(attempt, dict) and self._parse_timestamp(attempt.get("timestamp")) >= cutoff_time
493
+ ]
494
+
495
+ # Return count of attempts within rolling window
496
+ return len(window_attempts)
421
497
 
422
498
  def get_pr_attempt_list(self, pr_number: Union[int, str], repo: str = None, branch: str = None):
423
499
  """Get list of attempts for a specific PR (for detailed analysis)"""
@@ -436,7 +512,7 @@ class AutomationSafetyManager:
436
512
  # Create attempt record
437
513
  attempt_record = {
438
514
  "result": result,
439
- "timestamp": datetime.now().isoformat(),
515
+ "timestamp": datetime.now(timezone.utc).isoformat(),
440
516
  "pr_number": pr_number,
441
517
  "repo": repo,
442
518
  "branch": branch
@@ -547,7 +623,8 @@ class AutomationSafetyManager:
547
623
  return False
548
624
 
549
625
  try:
550
- approval_date = REAL_DATETIME.fromisoformat(approval_date_str)
626
+ # Use replace(tzinfo=None) for compatibility with datetime.now()
627
+ approval_date = REAL_DATETIME.fromisoformat(approval_date_str).replace(tzinfo=None)
551
628
  except (TypeError, ValueError):
552
629
  return False
553
630
  expiry = approval_date + timedelta(hours=self.approval_hours)
@@ -39,6 +39,8 @@ FIX_COMMENT_RUN_MARKER_SUFFIX = "-->"
39
39
  # Updated to match new format from build_automation_marker()
40
40
  FIXPR_MARKER_PREFIX = "<!-- fixpr-run-automation-commit:"
41
41
  FIXPR_MARKER_SUFFIX = "-->"
42
+ COMMENT_VALIDATION_MARKER_PREFIX = "<!-- comment-validation-commit:"
43
+ COMMENT_VALIDATION_MARKER_SUFFIX = "-->"
42
44
 
43
45
 
44
46
  def build_automation_marker(workflow: str, agent: str, commit_sha: str) -> str: