claude-dev-env 8.47.0 → 8.47.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/second-account-workers/SKILL.md +2 -1
- package/package.json +1 -1
- package/scripts/claude_account_worker.py +5 -2
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -0
- package/scripts/test_claude_account_worker.py +14 -0
- package/scripts/tests/test_pr_verification_lock.py +29 -4
|
@@ -55,7 +55,8 @@ When the command exits 3 and the report has `"account": "wait"`, no account is
|
|
|
55
55
|
eligible. Report the picker's reason and reset time. Do not fall back to the
|
|
56
56
|
Agent tool. Claude can also return exit 3 after a worker starts. When the
|
|
57
57
|
report account is `main`, `second`, or `extra_2` and later, report the child's
|
|
58
|
-
exit code and result.
|
|
58
|
+
exit code and result. Exit 124 with reason `timeout` means the worker ran past
|
|
59
|
+
`--timeout-minutes`.
|
|
59
60
|
|
|
60
61
|
Workers never commit, push, or call `gh`. The calling session reviews each
|
|
61
62
|
worktree diff and owns every Git step.
|
package/package.json
CHANGED
|
@@ -27,6 +27,8 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
27
27
|
PROMPT_FILE_FLAG,
|
|
28
28
|
REPORT_FILE_FLAG,
|
|
29
29
|
SINGLE_PROMPT_FLAG,
|
|
30
|
+
TIMEOUT_ATTEMPT_STATUS,
|
|
31
|
+
TIMEOUT_EXIT_CODE,
|
|
30
32
|
TIMEOUT_MINUTES_FLAG,
|
|
31
33
|
UTF8_ENCODING,
|
|
32
34
|
)
|
|
@@ -102,10 +104,11 @@ def run_worker(
|
|
|
102
104
|
if outcome.status in {"wait", "exhausted"}:
|
|
103
105
|
report = wait_report("wait", _wait_reason(outcome), outcome.wait_reset_at)
|
|
104
106
|
return finalize_report(report_file, report)
|
|
107
|
+
is_timed_out = bool(outcome.attempts) and outcome.attempts[-1][1] == TIMEOUT_ATTEMPT_STATUS
|
|
105
108
|
report = make_report(
|
|
106
109
|
outcome.account_name or "none",
|
|
107
|
-
outcome.status,
|
|
108
|
-
exit_code=outcome.returncode,
|
|
110
|
+
TIMEOUT_ATTEMPT_STATUS if is_timed_out else outcome.status,
|
|
111
|
+
exit_code=TIMEOUT_EXIT_CODE if is_timed_out else outcome.returncode,
|
|
109
112
|
duration_seconds=duration_seconds,
|
|
110
113
|
stdout_text=outcome.stdout,
|
|
111
114
|
)
|
|
@@ -13,6 +13,10 @@ MINIMUM_TIMEOUT_MINUTES: int = 1
|
|
|
13
13
|
|
|
14
14
|
LAUNCH_FAILURE_EXIT_CODE: int = 1
|
|
15
15
|
|
|
16
|
+
TIMEOUT_EXIT_CODE: int = 124
|
|
17
|
+
|
|
18
|
+
TIMEOUT_ATTEMPT_STATUS: str = "timeout"
|
|
19
|
+
|
|
16
20
|
CONFIGURATION_FAILURE_EXIT_CODE: int = 2
|
|
17
21
|
|
|
18
22
|
WAIT_DURATION_SECONDS: float = 0.0
|
|
@@ -100,6 +104,8 @@ __all__ = [
|
|
|
100
104
|
"SINGLE_PROMPT_FLAG",
|
|
101
105
|
"STDOUT_TAIL_CHARACTER_LIMIT",
|
|
102
106
|
"SUMMARY_LINE_TEMPLATE",
|
|
107
|
+
"TIMEOUT_ATTEMPT_STATUS",
|
|
108
|
+
"TIMEOUT_EXIT_CODE",
|
|
103
109
|
"TIMEOUT_MINUTES_FLAG",
|
|
104
110
|
"UTF8_ENCODING",
|
|
105
111
|
"WAIT_DURATION_SECONDS",
|
|
@@ -152,6 +152,20 @@ def test_should_preserve_served_failure_code(
|
|
|
152
152
|
assert report["is_error"] is True
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def test_should_exit_124_with_timeout_reason_when_the_broker_attempt_timed_out(
|
|
156
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
157
|
+
) -> None:
|
|
158
|
+
timed_out = JobOutcome(127, "", "timed out", "extra_2", (("extra_2", "timeout"),), "advisor_blocked", None, None)
|
|
159
|
+
|
|
160
|
+
exit_code, report, _ = _run_worker(monkeypatch, tmp_path, timed_out)
|
|
161
|
+
|
|
162
|
+
assert exit_code == 124
|
|
163
|
+
assert report["exit_code"] == 124
|
|
164
|
+
assert report["reason"] == "timeout"
|
|
165
|
+
assert report["account"] == "extra_2"
|
|
166
|
+
assert report["is_error"] is True
|
|
167
|
+
|
|
168
|
+
|
|
155
169
|
def test_should_write_report_for_unreadable_prompt(
|
|
156
170
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
157
171
|
) -> None:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import cast
|
|
@@ -50,6 +51,7 @@ RESULT_WAIT_SECONDS = 5
|
|
|
50
51
|
PROCESS_WAIT_SECONDS = 10
|
|
51
52
|
POLL_INTERVAL_SECONDS = 0.01
|
|
52
53
|
LEGACY_LOCK_CONTENT = "legacy owner metadata"
|
|
54
|
+
DELAYED_OUTCOME_WRITE_SECONDS = 0.2
|
|
53
55
|
BASE_PYTHON_EXECUTABLE = cast(str, getattr(sys, "_base_executable", sys.executable))
|
|
54
56
|
|
|
55
57
|
|
|
@@ -72,13 +74,20 @@ def _start_lock_contender(
|
|
|
72
74
|
)
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
def _read_reported_outcome(result_path: Path) -> str:
|
|
78
|
+
if not result_path.is_file():
|
|
79
|
+
return ""
|
|
80
|
+
return result_path.read_text(encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
|
|
75
83
|
def _wait_for_lock_outcomes(all_result_paths: list[Path]) -> list[str]:
|
|
76
84
|
deadline = time.monotonic() + RESULT_WAIT_SECONDS
|
|
77
85
|
while time.monotonic() < deadline:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
all_outcomes = [
|
|
87
|
+
_read_reported_outcome(each_path) for each_path in all_result_paths
|
|
88
|
+
]
|
|
89
|
+
if all(all_outcomes):
|
|
90
|
+
return all_outcomes
|
|
82
91
|
time.sleep(POLL_INTERVAL_SECONDS)
|
|
83
92
|
raise AssertionError("lock contenders did not report within five seconds")
|
|
84
93
|
|
|
@@ -146,6 +155,22 @@ def test_process_lock_survives_contention_and_owner_termination(
|
|
|
146
155
|
_terminate_owned_child(each_child_process)
|
|
147
156
|
|
|
148
157
|
|
|
158
|
+
def test_wait_for_lock_outcomes_waits_for_written_content(tmp_path: Path) -> None:
|
|
159
|
+
result_path = tmp_path / "delayed.result"
|
|
160
|
+
result_path.touch()
|
|
161
|
+
delayed_writer = threading.Timer(
|
|
162
|
+
DELAYED_OUTCOME_WRITE_SECONDS,
|
|
163
|
+
result_path.write_text,
|
|
164
|
+
args=("acquired",),
|
|
165
|
+
kwargs={"encoding": "utf-8"},
|
|
166
|
+
)
|
|
167
|
+
delayed_writer.start()
|
|
168
|
+
try:
|
|
169
|
+
assert _wait_for_lock_outcomes([result_path]) == ["acquired"]
|
|
170
|
+
finally:
|
|
171
|
+
delayed_writer.join()
|
|
172
|
+
|
|
173
|
+
|
|
149
174
|
def test_lock_io_error_is_presented_as_supervisor_lock_error(
|
|
150
175
|
tmp_path: Path,
|
|
151
176
|
) -> None:
|