claude-dev-env 8.47.1 → 8.47.3
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/hooks/blocking/bash_pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +16 -0
- package/package.json +1 -1
- package/scripts/ci/install-playtest.mjs +2 -3
- 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
|
@@ -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.
|
|
@@ -207,17 +207,21 @@ def aggregate_bash_hook_results(
|
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _emit_decision(decision: BashDispatcherDecision) -> None:
|
|
210
|
-
"""Write one PreToolUse permission payload to stdout when an outcome exists.
|
|
210
|
+
"""Write one PreToolUse permission payload to stdout when an outcome exists.
|
|
211
|
+
|
|
212
|
+
A rewrite carries updatedInput with no permissionDecision, so the harness
|
|
213
|
+
applies the rewritten command and then runs its normal permission prompt.
|
|
214
|
+
"""
|
|
211
215
|
if not decision.decision:
|
|
212
216
|
return
|
|
213
|
-
hook_specific: dict[str, object] = {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
hook_specific: dict[str, object] = {"hookEventName": HOOK_EVENT_NAME}
|
|
218
|
+
is_rewrite = decision.decision == ALLOW_DECISION and decision.updated_input is not None
|
|
219
|
+
if is_rewrite:
|
|
220
|
+
hook_specific[_UPDATED_INPUT_KEY] = decision.updated_input
|
|
221
|
+
else:
|
|
222
|
+
hook_specific[_PERMISSION_DECISION_KEY] = decision.decision
|
|
217
223
|
if decision.reasons:
|
|
218
224
|
hook_specific[_PERMISSION_REASON_KEY] = REASON_JOIN_SEPARATOR.join(decision.reasons)
|
|
219
|
-
if decision.decision == ALLOW_DECISION and decision.updated_input is not None:
|
|
220
|
-
hook_specific[_UPDATED_INPUT_KEY] = decision.updated_input
|
|
221
225
|
if decision.all_additional_context:
|
|
222
226
|
hook_specific[_ADDITIONAL_CONTEXT_KEY] = CONTEXT_JOIN_SEPARATOR.join(
|
|
223
227
|
decision.all_additional_context
|
|
@@ -235,3 +235,19 @@ def test_emit_decision_re_emits_silent_deny_fields(
|
|
|
235
235
|
assert hook_specific["additionalContext"] == "see docs/runbook.md"
|
|
236
236
|
assert emitted_payload["systemMessage"] == "[gh-gate] blocked redirected gh pr create"
|
|
237
237
|
assert emitted_payload["suppressOutput"] is True
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_emit_decision_sends_a_rewrite_through_the_permission_prompt(
|
|
241
|
+
capsys: pytest.CaptureFixture[str],
|
|
242
|
+
) -> None:
|
|
243
|
+
"""A rewritten command reaches the harness with updatedInput and no permissionDecision."""
|
|
244
|
+
rewrite = BashDispatcherDecision(
|
|
245
|
+
decision="allow",
|
|
246
|
+
updated_input={"command": "MSYS2_ARG_CONV_EXCL='*' git show HEAD:a.py"},
|
|
247
|
+
)
|
|
248
|
+
_emit_decision(rewrite)
|
|
249
|
+
hook_specific = json.loads(capsys.readouterr().out.strip())["hookSpecificOutput"]
|
|
250
|
+
assert "permissionDecision" not in hook_specific
|
|
251
|
+
assert hook_specific["updatedInput"] == {
|
|
252
|
+
"command": "MSYS2_ARG_CONV_EXCL='*' git show HEAD:a.py"
|
|
253
|
+
}
|
package/package.json
CHANGED
|
@@ -54,7 +54,6 @@ export const PRE_TOOL_USE_EVENT_NAME = 'PreToolUse';
|
|
|
54
54
|
export const SESSION_START_HOOK_SCRIPT_NAME = 'working_style_prompt.py';
|
|
55
55
|
export const BASH_HOOK_SCRIPT_NAME = 'bash_pre_tool_use_dispatcher.py';
|
|
56
56
|
export const BASH_MATCHER = 'Bash';
|
|
57
|
-
export const ALLOW_DECISION = 'allow';
|
|
58
57
|
|
|
59
58
|
export const SESSION_START_PAYLOAD = {
|
|
60
59
|
hook_event_name: SESSION_START_EVENT_NAME,
|
|
@@ -238,8 +237,8 @@ function runBlockingStage(settingsPath, scratchHome, evidenceDirectory) {
|
|
|
238
237
|
);
|
|
239
238
|
const hookResult = runInstalledHook(command, scratchHome, BASH_HOOK_PAYLOAD);
|
|
240
239
|
const envelope = readEnvelope(hookResult.stdout, PRE_TOOL_USE_EVENT_NAME);
|
|
241
|
-
if (envelope.permissionDecision !==
|
|
242
|
-
throw new Error(`The PreToolUse envelope
|
|
240
|
+
if (envelope.permissionDecision !== undefined) {
|
|
241
|
+
throw new Error(`The PreToolUse envelope skipped the permission prompt with ${envelope.permissionDecision}`);
|
|
243
242
|
}
|
|
244
243
|
const rewrittenCommand = envelope.updatedInput?.command ?? '';
|
|
245
244
|
if (!rewrittenCommand.includes(REWRITE_MARKER)) {
|
|
@@ -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:
|