jleechanorg-pr-automation 0.1.1__py3-none-any.whl → 0.2.45__py3-none-any.whl
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.
- jleechanorg_pr_automation/STORAGE_STATE_TESTING_PROTOCOL.md +326 -0
- jleechanorg_pr_automation/__init__.py +64 -9
- jleechanorg_pr_automation/automation_safety_manager.py +306 -95
- jleechanorg_pr_automation/automation_safety_wrapper.py +13 -19
- jleechanorg_pr_automation/automation_utils.py +87 -65
- jleechanorg_pr_automation/check_codex_comment.py +7 -1
- jleechanorg_pr_automation/codex_branch_updater.py +21 -9
- jleechanorg_pr_automation/codex_config.py +70 -3
- jleechanorg_pr_automation/jleechanorg_pr_monitor.py +1954 -234
- jleechanorg_pr_automation/logging_utils.py +86 -0
- jleechanorg_pr_automation/openai_automation/__init__.py +3 -0
- jleechanorg_pr_automation/openai_automation/codex_github_mentions.py +1111 -0
- jleechanorg_pr_automation/openai_automation/debug_page_content.py +88 -0
- jleechanorg_pr_automation/openai_automation/oracle_cli.py +364 -0
- jleechanorg_pr_automation/openai_automation/test_auth_restoration.py +244 -0
- jleechanorg_pr_automation/openai_automation/test_codex_comprehensive.py +355 -0
- jleechanorg_pr_automation/openai_automation/test_codex_integration.py +254 -0
- jleechanorg_pr_automation/orchestrated_pr_runner.py +516 -0
- jleechanorg_pr_automation/tests/__init__.py +0 -0
- jleechanorg_pr_automation/tests/test_actionable_counting_matrix.py +84 -86
- jleechanorg_pr_automation/tests/test_attempt_limit_logic.py +124 -0
- jleechanorg_pr_automation/tests/test_automation_marker_functions.py +175 -0
- jleechanorg_pr_automation/tests/test_automation_over_running_reproduction.py +9 -11
- jleechanorg_pr_automation/tests/test_automation_safety_limits.py +91 -79
- jleechanorg_pr_automation/tests/test_automation_safety_manager_comprehensive.py +53 -53
- jleechanorg_pr_automation/tests/test_codex_actor_matching.py +1 -1
- jleechanorg_pr_automation/tests/test_fixpr_prompt.py +54 -0
- jleechanorg_pr_automation/tests/test_fixpr_return_value.py +140 -0
- jleechanorg_pr_automation/tests/test_graphql_error_handling.py +26 -26
- jleechanorg_pr_automation/tests/test_model_parameter.py +317 -0
- jleechanorg_pr_automation/tests/test_orchestrated_pr_runner.py +697 -0
- jleechanorg_pr_automation/tests/test_packaging_integration.py +127 -0
- jleechanorg_pr_automation/tests/test_pr_filtering_matrix.py +246 -193
- jleechanorg_pr_automation/tests/test_pr_monitor_eligibility.py +354 -0
- jleechanorg_pr_automation/tests/test_pr_targeting.py +102 -7
- jleechanorg_pr_automation/tests/test_version_consistency.py +51 -0
- jleechanorg_pr_automation/tests/test_workflow_specific_limits.py +202 -0
- jleechanorg_pr_automation/tests/test_workspace_dispatch_missing_dir.py +119 -0
- jleechanorg_pr_automation/utils.py +81 -56
- jleechanorg_pr_automation-0.2.45.dist-info/METADATA +864 -0
- jleechanorg_pr_automation-0.2.45.dist-info/RECORD +45 -0
- jleechanorg_pr_automation-0.1.1.dist-info/METADATA +0 -222
- jleechanorg_pr_automation-0.1.1.dist-info/RECORD +0 -23
- {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/WHEEL +0 -0
- {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/entry_points.txt +0 -0
- {jleechanorg_pr_automation-0.1.1.dist-info → jleechanorg_pr_automation-0.2.45.dist-info}/top_level.txt +0 -0
|
@@ -3,33 +3,27 @@
|
|
|
3
3
|
Automation Safety Wrapper for launchd
|
|
4
4
|
|
|
5
5
|
This wrapper enforces safety limits before running PR automation:
|
|
6
|
-
- Max
|
|
6
|
+
- Max 10 attempts per PR
|
|
7
7
|
- Max 50 total automation runs before requiring manual approval
|
|
8
8
|
- Email notifications when limits are reached
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import logging
|
|
12
12
|
import os
|
|
13
13
|
import subprocess
|
|
14
|
-
import
|
|
14
|
+
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
|
|
16
17
|
from .automation_safety_manager import AutomationSafetyManager
|
|
18
|
+
from .logging_utils import setup_logging as _setup_logging
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def setup_logging() -> logging.Logger:
|
|
20
|
-
"""Set up logging
|
|
22
|
+
"""Set up logging delegated to centralized logging_utils"""
|
|
21
23
|
log_dir = Path.home() / "Library" / "Logs" / "worldarchitect-automation"
|
|
22
|
-
log_dir.
|
|
24
|
+
log_file = log_dir / "automation_safety.log"
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
level=logging.INFO,
|
|
26
|
-
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
27
|
-
handlers=[
|
|
28
|
-
logging.FileHandler(log_dir / "automation_safety.log"),
|
|
29
|
-
logging.StreamHandler()
|
|
30
|
-
]
|
|
31
|
-
)
|
|
32
|
-
return logging.getLogger(__name__)
|
|
26
|
+
return _setup_logging(__name__, log_file=str(log_file))
|
|
33
27
|
|
|
34
28
|
|
|
35
29
|
def main() -> int:
|
|
@@ -66,8 +60,8 @@ def main() -> int:
|
|
|
66
60
|
|
|
67
61
|
# Execute with environment variables for safety integration
|
|
68
62
|
env = os.environ.copy()
|
|
69
|
-
env[
|
|
70
|
-
env[
|
|
63
|
+
env["AUTOMATION_SAFETY_DATA_DIR"] = str(data_dir)
|
|
64
|
+
env["AUTOMATION_SAFETY_WRAPPER"] = "1"
|
|
71
65
|
|
|
72
66
|
# Record the global run *before* launching the monitor so the attempt
|
|
73
67
|
# is counted even if the subprocess fails to start or exits early.
|
|
@@ -78,8 +72,8 @@ def main() -> int:
|
|
|
78
72
|
)
|
|
79
73
|
|
|
80
74
|
result = subprocess.run(
|
|
81
|
-
[sys.executable,
|
|
82
|
-
env=env,
|
|
75
|
+
[sys.executable, "-m", "jleechanorg_pr_automation.jleechanorg_pr_monitor"],
|
|
76
|
+
check=False, env=env,
|
|
83
77
|
capture_output=True,
|
|
84
78
|
text=True,
|
|
85
79
|
timeout=3600,
|
|
@@ -112,5 +106,5 @@ def main() -> int:
|
|
|
112
106
|
manager.check_and_notify_limits()
|
|
113
107
|
|
|
114
108
|
|
|
115
|
-
if __name__ ==
|
|
109
|
+
if __name__ == "__main__":
|
|
116
110
|
sys.exit(main())
|
|
@@ -10,20 +10,21 @@ Consolidates common functionality used across automation components:
|
|
|
10
10
|
- File and directory management utilities
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import sys
|
|
13
|
+
import fcntl
|
|
15
14
|
import json
|
|
16
15
|
import logging
|
|
16
|
+
import os
|
|
17
17
|
import smtplib
|
|
18
|
-
import tempfile
|
|
19
|
-
import threading
|
|
20
|
-
import fcntl
|
|
21
18
|
import subprocess
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
22
21
|
from datetime import datetime
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import Dict, Optional, Tuple, Any
|
|
25
|
-
from email.mime.text import MIMEText
|
|
26
22
|
from email.mime.multipart import MIMEMultipart
|
|
23
|
+
from email.mime.text import MIMEText
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, Optional, Sequence, Tuple
|
|
26
|
+
|
|
27
|
+
from .logging_utils import setup_logging as _setup_logging
|
|
27
28
|
|
|
28
29
|
try:
|
|
29
30
|
import keyring
|
|
@@ -37,17 +38,17 @@ class AutomationUtils:
|
|
|
37
38
|
|
|
38
39
|
# Default configuration
|
|
39
40
|
DEFAULT_CONFIG = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
"SMTP_SERVER": "smtp.gmail.com",
|
|
42
|
+
"SMTP_PORT": 587,
|
|
43
|
+
"LOG_DIR": "~/Library/Logs/worldarchitect-automation",
|
|
44
|
+
"DATA_DIR": "~/Library/Application Support/worldarchitect-automation",
|
|
45
|
+
"MAX_SUBPROCESS_TIMEOUT": int(os.getenv("AUTOMATION_SUBPROCESS_TIMEOUT", "300")), # 5 minutes (configurable)
|
|
46
|
+
"EMAIL_SUBJECT_PREFIX": "[WorldArchitect Automation]"
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
@classmethod
|
|
49
50
|
def setup_logging(cls, name: str, log_filename: str = None) -> logging.Logger:
|
|
50
|
-
"""Unified logging setup
|
|
51
|
+
"""Unified logging setup delegated to centralized logging_utils.
|
|
51
52
|
|
|
52
53
|
Args:
|
|
53
54
|
name: Logger name (typically __name__)
|
|
@@ -56,25 +57,17 @@ class AutomationUtils:
|
|
|
56
57
|
Returns:
|
|
57
58
|
Configured logger instance
|
|
58
59
|
"""
|
|
60
|
+
# Use centralized logging with DEFAULT_CONFIG log directory
|
|
61
|
+
log_dir = Path(cls.DEFAULT_CONFIG["LOG_DIR"]).expanduser()
|
|
62
|
+
|
|
63
|
+
# If no filename specified, create one from module name
|
|
59
64
|
if log_filename is None:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Set up logging with consistent format
|
|
67
|
-
logging.basicConfig(
|
|
68
|
-
level=logging.INFO,
|
|
69
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
70
|
-
handlers=[
|
|
71
|
-
logging.FileHandler(log_dir / log_filename),
|
|
72
|
-
logging.StreamHandler()
|
|
73
|
-
]
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
logger = logging.getLogger(name)
|
|
77
|
-
logger.info(f"🛠️ Logging initialized - logs: {log_dir / log_filename}")
|
|
65
|
+
log_file = log_dir / f"{name.split('.')[-1]}.log"
|
|
66
|
+
else:
|
|
67
|
+
log_file = log_dir / log_filename if not Path(log_filename).is_absolute() else log_filename
|
|
68
|
+
|
|
69
|
+
logger = _setup_logging(name, log_file=str(log_file))
|
|
70
|
+
logger.info(f"🛠️ Logging initialized - logs: {log_file}")
|
|
78
71
|
return logger
|
|
79
72
|
|
|
80
73
|
@classmethod
|
|
@@ -84,8 +77,8 @@ class AutomationUtils:
|
|
|
84
77
|
if env_value is not None:
|
|
85
78
|
# Try to convert to appropriate type
|
|
86
79
|
if isinstance(default, bool):
|
|
87
|
-
return env_value.lower() in (
|
|
88
|
-
|
|
80
|
+
return env_value.lower() in ("true", "1", "yes", "on")
|
|
81
|
+
if isinstance(default, int):
|
|
89
82
|
try:
|
|
90
83
|
return int(env_value)
|
|
91
84
|
except ValueError:
|
|
@@ -96,7 +89,7 @@ class AutomationUtils:
|
|
|
96
89
|
@classmethod
|
|
97
90
|
def get_data_directory(cls, subdir: str = None) -> Path:
|
|
98
91
|
"""Get standardized data directory path"""
|
|
99
|
-
data_dir = Path(cls.DEFAULT_CONFIG[
|
|
92
|
+
data_dir = Path(cls.DEFAULT_CONFIG["DATA_DIR"]).expanduser()
|
|
100
93
|
if subdir:
|
|
101
94
|
data_dir = data_dir / subdir
|
|
102
95
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -117,9 +110,9 @@ class AutomationUtils:
|
|
|
117
110
|
|
|
118
111
|
# Fallback to environment variables if keyring fails or unavailable
|
|
119
112
|
if not username:
|
|
120
|
-
username = os.environ.get(
|
|
113
|
+
username = os.environ.get("SMTP_USERNAME")
|
|
121
114
|
if not password:
|
|
122
|
-
password = os.environ.get(
|
|
115
|
+
password = os.environ.get("SMTP_PASSWORD")
|
|
123
116
|
|
|
124
117
|
return username, password
|
|
125
118
|
|
|
@@ -139,13 +132,13 @@ class AutomationUtils:
|
|
|
139
132
|
"""
|
|
140
133
|
try:
|
|
141
134
|
# Get SMTP configuration
|
|
142
|
-
smtp_server = cls.get_config_value(
|
|
143
|
-
smtp_port = cls.get_config_value(
|
|
135
|
+
smtp_server = cls.get_config_value("SMTP_SERVER")
|
|
136
|
+
smtp_port = cls.get_config_value("SMTP_PORT")
|
|
144
137
|
username, password = cls.get_smtp_credentials()
|
|
145
138
|
|
|
146
139
|
# Get email addresses
|
|
147
|
-
from_email = from_email or os.environ.get(
|
|
148
|
-
to_email = to_email or os.environ.get(
|
|
140
|
+
from_email = from_email or os.environ.get("MEMORY_EMAIL_FROM")
|
|
141
|
+
to_email = to_email or os.environ.get("MEMORY_EMAIL_TO")
|
|
149
142
|
|
|
150
143
|
if not all([username, password, from_email, to_email]):
|
|
151
144
|
print("Email configuration incomplete - skipping notification")
|
|
@@ -153,9 +146,9 @@ class AutomationUtils:
|
|
|
153
146
|
|
|
154
147
|
# Build email message
|
|
155
148
|
msg = MIMEMultipart()
|
|
156
|
-
msg[
|
|
157
|
-
msg[
|
|
158
|
-
msg[
|
|
149
|
+
msg["From"] = from_email
|
|
150
|
+
msg["To"] = to_email
|
|
151
|
+
msg["Subject"] = f"{cls.DEFAULT_CONFIG['EMAIL_SUBJECT_PREFIX']} {subject}"
|
|
159
152
|
|
|
160
153
|
# Add timestamp to message
|
|
161
154
|
full_message = f"""{message}
|
|
@@ -165,7 +158,7 @@ System: WorldArchitect Automation
|
|
|
165
158
|
|
|
166
159
|
This is an automated notification from the WorldArchitect.AI automation system."""
|
|
167
160
|
|
|
168
|
-
msg.attach(MIMEText(full_message,
|
|
161
|
+
msg.attach(MIMEText(full_message, "plain"))
|
|
169
162
|
|
|
170
163
|
# Send email with timeout and proper error handling
|
|
171
164
|
with smtplib.SMTP(smtp_server, smtp_port, timeout=30) as server:
|
|
@@ -192,7 +185,11 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
192
185
|
@classmethod
|
|
193
186
|
def execute_subprocess_with_timeout(cls, command: list, timeout: int = None,
|
|
194
187
|
cwd: str = None, capture_output: bool = True,
|
|
195
|
-
check: bool = True
|
|
188
|
+
check: bool = True,
|
|
189
|
+
retry_attempts: int = 1,
|
|
190
|
+
retry_backoff_seconds: float = 1.0,
|
|
191
|
+
retry_backoff_multiplier: float = 2.0,
|
|
192
|
+
retry_on_stderr_substrings: Optional[Sequence[str]] = None) -> subprocess.CompletedProcess:
|
|
196
193
|
"""Execute subprocess with standardized timeout and error handling
|
|
197
194
|
|
|
198
195
|
Args:
|
|
@@ -201,6 +198,10 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
201
198
|
cwd: Working directory
|
|
202
199
|
capture_output: Whether to capture stdout/stderr
|
|
203
200
|
check: Whether to raise CalledProcessError on non-zero exit (default True)
|
|
201
|
+
retry_attempts: Total attempts for retryable failures (default 1 = no retries)
|
|
202
|
+
retry_backoff_seconds: Initial backoff between retries
|
|
203
|
+
retry_backoff_multiplier: Backoff multiplier per attempt
|
|
204
|
+
retry_on_stderr_substrings: If provided, only retry when stderr/stdout contains one of these substrings
|
|
204
205
|
|
|
205
206
|
Returns:
|
|
206
207
|
CompletedProcess instance
|
|
@@ -210,26 +211,47 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
210
211
|
subprocess.CalledProcessError: If command fails and check=True
|
|
211
212
|
"""
|
|
212
213
|
if timeout is None:
|
|
213
|
-
timeout = cls.get_config_value(
|
|
214
|
+
timeout = cls.get_config_value("MAX_SUBPROCESS_TIMEOUT")
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
retry_attempts_int = int(retry_attempts)
|
|
218
|
+
except (TypeError, ValueError):
|
|
219
|
+
retry_attempts_int = 1
|
|
220
|
+
if retry_attempts_int < 1:
|
|
221
|
+
retry_attempts_int = 1
|
|
222
|
+
|
|
223
|
+
attempt = 1
|
|
224
|
+
while True:
|
|
225
|
+
try:
|
|
226
|
+
# Ensure shell=False for security, check parameter controls error handling
|
|
227
|
+
return subprocess.run(
|
|
228
|
+
command,
|
|
229
|
+
timeout=timeout,
|
|
230
|
+
cwd=cwd,
|
|
231
|
+
capture_output=capture_output,
|
|
232
|
+
text=True,
|
|
233
|
+
shell=False,
|
|
234
|
+
check=check,
|
|
235
|
+
)
|
|
236
|
+
except subprocess.CalledProcessError as exc:
|
|
237
|
+
if attempt >= retry_attempts_int:
|
|
238
|
+
raise
|
|
214
239
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
cwd=cwd,
|
|
220
|
-
capture_output=capture_output,
|
|
221
|
-
text=True,
|
|
222
|
-
shell=False,
|
|
223
|
-
check=check
|
|
224
|
-
)
|
|
240
|
+
if retry_on_stderr_substrings:
|
|
241
|
+
stderr = (exc.stderr or "") + "\n" + (exc.stdout or "")
|
|
242
|
+
if not any(token in stderr for token in retry_on_stderr_substrings):
|
|
243
|
+
raise
|
|
225
244
|
|
|
226
|
-
|
|
245
|
+
delay = float(retry_backoff_seconds) * (float(retry_backoff_multiplier) ** (attempt - 1))
|
|
246
|
+
delay = max(0.0, min(delay, 60.0))
|
|
247
|
+
time.sleep(delay)
|
|
248
|
+
attempt += 1
|
|
227
249
|
|
|
228
250
|
@classmethod
|
|
229
251
|
def safe_read_json(cls, file_path: Path) -> dict:
|
|
230
252
|
"""Safely read JSON file with file locking"""
|
|
231
253
|
try:
|
|
232
|
-
with open(file_path
|
|
254
|
+
with open(file_path) as f:
|
|
233
255
|
fcntl.flock(f.fileno(), fcntl.LOCK_SH) # Shared lock for reading
|
|
234
256
|
data = json.load(f)
|
|
235
257
|
fcntl.flock(f.fileno(), fcntl.LOCK_UN) # Unlock
|
|
@@ -245,7 +267,7 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
245
267
|
try:
|
|
246
268
|
# Create temporary file securely
|
|
247
269
|
with tempfile.NamedTemporaryFile(
|
|
248
|
-
mode=
|
|
270
|
+
mode="w",
|
|
249
271
|
suffix=".tmp",
|
|
250
272
|
delete=False
|
|
251
273
|
) as temp_file:
|
|
@@ -263,7 +285,7 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
263
285
|
os.rename(temp_path, file_path)
|
|
264
286
|
temp_path = None # Successful, don't clean up
|
|
265
287
|
|
|
266
|
-
except (OSError,
|
|
288
|
+
except (OSError, json.JSONEncodeError) as e:
|
|
267
289
|
# Clean up temp file on error
|
|
268
290
|
if temp_path and os.path.exists(temp_path):
|
|
269
291
|
try:
|
|
@@ -282,13 +304,13 @@ This is an automated notification from the WorldArchitect.AI automation system."
|
|
|
282
304
|
try:
|
|
283
305
|
# Source the bash config file by running it and capturing environment
|
|
284
306
|
result = cls.execute_subprocess_with_timeout(
|
|
285
|
-
[
|
|
307
|
+
["bash", "-c", f"source {config_file} && env"],
|
|
286
308
|
timeout=10
|
|
287
309
|
)
|
|
288
310
|
|
|
289
311
|
for line in result.stdout.splitlines():
|
|
290
|
-
if
|
|
291
|
-
key, value = line.split(
|
|
312
|
+
if "=" in line:
|
|
313
|
+
key, value = line.split("=", 1)
|
|
292
314
|
config[key] = value
|
|
293
315
|
|
|
294
316
|
except Exception as e:
|
|
@@ -55,7 +55,13 @@ def decide(marker_prefix: str, marker_suffix: str) -> Tuple[str, str]:
|
|
|
55
55
|
if end_index == -1:
|
|
56
56
|
continue
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
marker_content = body[start_index:end_index].strip()
|
|
59
|
+
# Handle new format: SHA:cli -> extract just SHA
|
|
60
|
+
# Also handles old format: SHA (no colon)
|
|
61
|
+
if ":" in marker_content:
|
|
62
|
+
marker_sha = marker_content.split(":")[0]
|
|
63
|
+
else:
|
|
64
|
+
marker_sha = marker_content
|
|
59
65
|
if marker_sha == head_sha:
|
|
60
66
|
return "skip", head_sha
|
|
61
67
|
|
|
@@ -13,18 +13,22 @@ from typing import Dict, Tuple
|
|
|
13
13
|
from playwright.async_api import (
|
|
14
14
|
Browser,
|
|
15
15
|
BrowserContext,
|
|
16
|
-
Error as PlaywrightError,
|
|
17
16
|
Page,
|
|
18
|
-
TimeoutError as PlaywrightTimeoutError,
|
|
19
17
|
async_playwright,
|
|
20
18
|
)
|
|
19
|
+
from playwright.async_api import (
|
|
20
|
+
Error as PlaywrightError,
|
|
21
|
+
)
|
|
22
|
+
from playwright.async_api import (
|
|
23
|
+
TimeoutError as PlaywrightTimeoutError,
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
CHATGPT_CODEX_URL = "https://chatgpt.com/codex"
|
|
23
27
|
CREDENTIALS_PATH = Path.home() / ".chatgpt_codex_credentials.json"
|
|
24
28
|
AUTH_STATE_PATH = Path.home() / ".chatgpt_codex_auth_state.json"
|
|
25
|
-
TASK_CARD_SELECTOR =
|
|
26
|
-
UPDATE_BRANCH_BUTTON_SELECTOR =
|
|
27
|
-
TASK_TITLE_SELECTOR =
|
|
29
|
+
TASK_CARD_SELECTOR = '[data-testid="codex-task-card"]'
|
|
30
|
+
UPDATE_BRANCH_BUTTON_SELECTOR = 'button:has-text("Update Branch")'
|
|
31
|
+
TASK_TITLE_SELECTOR = '[data-testid="codex-task-title"]'
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def _save_credentials(credentials: Dict[str, str]) -> None:
|
|
@@ -76,14 +80,16 @@ def get_credentials() -> Dict[str, str]:
|
|
|
76
80
|
return credentials
|
|
77
81
|
|
|
78
82
|
|
|
79
|
-
async def ensure_logged_in(page: Page, credentials: Dict[str, str] | None = None) -> None:
|
|
80
|
-
"""Log into ChatGPT Codex if required."""
|
|
83
|
+
async def ensure_logged_in(page: Page, context: BrowserContext, credentials: Dict[str, str] | None = None) -> None:
|
|
84
|
+
"""Log into ChatGPT Codex if required and save auth state immediately."""
|
|
81
85
|
|
|
82
86
|
await page.goto(CHATGPT_CODEX_URL, wait_until="domcontentloaded")
|
|
83
87
|
|
|
84
88
|
if await is_task_list_visible(page):
|
|
89
|
+
print("✅ Session still valid (task list visible).")
|
|
85
90
|
return
|
|
86
91
|
|
|
92
|
+
print("⚠️ Session expired. Re-authenticating...")
|
|
87
93
|
if credentials is None:
|
|
88
94
|
credentials = get_credentials()
|
|
89
95
|
|
|
@@ -95,6 +101,9 @@ async def ensure_logged_in(page: Page, credentials: Dict[str, str] | None = None
|
|
|
95
101
|
|
|
96
102
|
await _complete_login_flow(page, credentials)
|
|
97
103
|
|
|
104
|
+
await context.storage_state(path=str(AUTH_STATE_PATH))
|
|
105
|
+
print(f"💾 New authentication state saved immediately to {AUTH_STATE_PATH}.")
|
|
106
|
+
|
|
98
107
|
|
|
99
108
|
async def _complete_login_flow(page: Page, credentials: Dict[str, str]) -> None:
|
|
100
109
|
"""Fill in the login flow using the provided credentials."""
|
|
@@ -244,16 +253,19 @@ async def run() -> None:
|
|
|
244
253
|
browser = await playwright.chromium.launch(headless=False)
|
|
245
254
|
context_kwargs = {}
|
|
246
255
|
if AUTH_STATE_PATH.exists():
|
|
256
|
+
print(f"🔄 Loading saved authentication state from {AUTH_STATE_PATH}")
|
|
247
257
|
context_kwargs["storage_state"] = str(AUTH_STATE_PATH)
|
|
258
|
+
else:
|
|
259
|
+
print("ℹ️ No saved authentication state found. Fresh login required.")
|
|
248
260
|
context = await browser.new_context(**context_kwargs)
|
|
249
261
|
page = await context.new_page()
|
|
250
262
|
|
|
251
|
-
await ensure_logged_in(page)
|
|
263
|
+
await ensure_logged_in(page, context)
|
|
252
264
|
await wait_for_task_list(page)
|
|
253
265
|
await process_tasks(page)
|
|
254
266
|
|
|
255
267
|
await context.storage_state(path=str(AUTH_STATE_PATH))
|
|
256
|
-
print(f"💾
|
|
268
|
+
print(f"💾 Final authentication state saved to {AUTH_STATE_PATH}.")
|
|
257
269
|
finally:
|
|
258
270
|
if context is not None:
|
|
259
271
|
await context.close()
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
DEFAULT_ASSISTANT_HANDLE = "coderabbitai"
|
|
9
8
|
|
|
10
9
|
|
|
@@ -25,13 +24,81 @@ CODEX_COMMENT_INTRO_BODY = (
|
|
|
25
24
|
CODEX_COMMENT_TEMPLATE = (
|
|
26
25
|
"{comment_intro}\n\n"
|
|
27
26
|
"Use your judgment to fix comments from everyone or explain why it should not be fixed. "
|
|
28
|
-
|
|
27
|
+
'Follow binary response protocol - every comment needs "DONE" or "NOT DONE" classification '
|
|
29
28
|
"explicitly with an explanation. Address all comments on this PR. Fix any failing tests and "
|
|
30
29
|
"resolve merge conflicts. Push any commits needed to remote so the PR is updated."
|
|
31
30
|
)
|
|
32
31
|
|
|
33
32
|
CODEX_COMMIT_MARKER_PREFIX = "<!-- codex-automation-commit:"
|
|
34
33
|
CODEX_COMMIT_MARKER_SUFFIX = "-->"
|
|
34
|
+
FIX_COMMENT_MARKER_PREFIX = "<!-- fix-comment-automation-commit:"
|
|
35
|
+
FIX_COMMENT_MARKER_SUFFIX = "-->"
|
|
36
|
+
# Updated to match new format from build_automation_marker()
|
|
37
|
+
FIX_COMMENT_RUN_MARKER_PREFIX = "<!-- fix-comment-run-automation-commit:"
|
|
38
|
+
FIX_COMMENT_RUN_MARKER_SUFFIX = "-->"
|
|
39
|
+
# Updated to match new format from build_automation_marker()
|
|
40
|
+
FIXPR_MARKER_PREFIX = "<!-- fixpr-run-automation-commit:"
|
|
41
|
+
FIXPR_MARKER_SUFFIX = "-->"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_automation_marker(workflow: str, agent: str, commit_sha: str) -> str:
|
|
45
|
+
"""Build enhanced automation marker with workflow, agent, and commit info.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
workflow: Workflow type (e.g., 'fix-comment-run', 'fixpr-run', 'codex')
|
|
49
|
+
agent: Agent/CLI name (e.g., 'gemini', 'codex', 'claude')
|
|
50
|
+
commit_sha: Git commit SHA
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
HTML comment marker with format: <!-- workflow-automation-commit:agent:sha -->
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> build_automation_marker('fix-comment-run', 'gemini', 'abc123')
|
|
57
|
+
'<!-- fix-comment-run-automation-commit:gemini:abc123-->'
|
|
58
|
+
"""
|
|
59
|
+
return f"<!-- {workflow}-automation-commit:{agent}:{commit_sha}-->"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_automation_marker(marker: str) -> dict[str, str] | None:
|
|
63
|
+
"""Parse automation marker to extract workflow, agent, and commit.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
marker: Automation marker string
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dict with 'workflow', 'agent', 'commit' keys, or None if invalid
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> parse_automation_marker('<!-- fix-comment-automation-commit:gemini:abc123-->')
|
|
73
|
+
{'workflow': 'fix-comment', 'agent': 'gemini', 'commit': 'abc123'}
|
|
74
|
+
"""
|
|
75
|
+
if not marker.startswith('<!--') or not marker.endswith('-->'):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Remove HTML comment markers (slice off "<!--" and "-->")
|
|
79
|
+
content = marker[4:-3].strip()
|
|
80
|
+
|
|
81
|
+
# Try new format first: workflow-automation-commit:agent:sha
|
|
82
|
+
if '-automation-commit:' in content and content.count(':') == 2:
|
|
83
|
+
parts = content.split(':')
|
|
84
|
+
workflow = parts[0].replace('-automation-commit', '')
|
|
85
|
+
return {
|
|
86
|
+
'workflow': workflow,
|
|
87
|
+
'agent': parts[1],
|
|
88
|
+
'commit': parts[2]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Legacy format: workflow-automation-commit:sha (no agent)
|
|
92
|
+
if '-automation-commit:' in content and content.count(':') == 1:
|
|
93
|
+
parts = content.split(':')
|
|
94
|
+
workflow = parts[0].replace('-automation-commit', '')
|
|
95
|
+
return {
|
|
96
|
+
'workflow': workflow,
|
|
97
|
+
'agent': 'unknown',
|
|
98
|
+
'commit': parts[1]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return None
|
|
35
102
|
|
|
36
103
|
|
|
37
104
|
def normalise_handle(assistant_handle: str | None) -> str:
|
|
@@ -115,7 +182,7 @@ class CodexConfig:
|
|
|
115
182
|
commit_marker_suffix: str = CODEX_COMMIT_MARKER_SUFFIX
|
|
116
183
|
|
|
117
184
|
@classmethod
|
|
118
|
-
def from_env(cls, assistant_handle: str | None) ->
|
|
185
|
+
def from_env(cls, assistant_handle: str | None) -> CodexConfig:
|
|
119
186
|
handle = normalise_handle(assistant_handle)
|
|
120
187
|
return cls(
|
|
121
188
|
assistant_handle=handle,
|