claude-dev-env 8.14.0 → 8.15.0
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/everything-search/SKILL.md +1 -1
- package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
- package/.agents/skills/second-account-workers/AGENTS.md +1 -0
- package/.agents/skills/second-account-workers/SKILL.md +65 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +42 -0
- package/docs/DJANGO_PATTERNS.md +2 -2
- package/docs/TEST_QUALITY.md +1 -13
- package/docs/second-claude-account.md +4 -0
- package/hooks/hooks.json +1 -6
- package/hooks/session/style_reminder_prompt.py +18 -4
- package/hooks/session/test_style_reminder_prompt.py +57 -3
- package/package.json +1 -1
- package/rules/research-mode.md +1 -1
- package/scripts/claude_account_worker.py +347 -0
- package/scripts/claude_account_worker_process.py +169 -0
- package/scripts/claude_account_worker_report.py +170 -0
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
- package/scripts/grok_headless_runner.py +2 -5
- package/scripts/subprocess_stream_text.py +28 -0
- package/scripts/task_usage_events.py +387 -0
- package/scripts/task_usage_report.py +391 -0
- package/scripts/test_claude_account_worker.py +331 -0
- package/scripts/test_task_usage_events.py +47 -0
- package/scripts/test_task_usage_report.py +603 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Launch a headless Claude worker process and wait for it to finish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
14
|
+
DRAIN_ATTEMPT_LIMIT,
|
|
15
|
+
DRAIN_GRACE_TIMEOUT_SECONDS,
|
|
16
|
+
LAUNCH_FAILURE_EXIT_CODE,
|
|
17
|
+
SECONDS_PER_MINUTE,
|
|
18
|
+
TIMEOUT_EXIT_CODE,
|
|
19
|
+
UTF8_DECODE_ERRORS,
|
|
20
|
+
UTF8_ENCODING,
|
|
21
|
+
)
|
|
22
|
+
from shared_tree_paths import resolve_shared_process_tree_scripts_directory
|
|
23
|
+
from subprocess_stream_text import decode_stream_text
|
|
24
|
+
from subprocess_window_access import hidden_window_creation_flags
|
|
25
|
+
|
|
26
|
+
_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
|
|
27
|
+
__file__, all_environment=os.environ
|
|
28
|
+
)
|
|
29
|
+
if str(_process_tree_scripts_directory) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_process_tree_scripts_directory))
|
|
31
|
+
|
|
32
|
+
_process_tree_kill = importlib.import_module("process_tree_kill")
|
|
33
|
+
_should_start_new_session = _process_tree_kill.should_start_new_session
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WorkerProcess(Protocol):
|
|
37
|
+
returncode: int | None
|
|
38
|
+
|
|
39
|
+
pid: int
|
|
40
|
+
|
|
41
|
+
def communicate(
|
|
42
|
+
self,
|
|
43
|
+
input: str | None = None,
|
|
44
|
+
timeout: float | None = None,
|
|
45
|
+
) -> tuple[str | bytes | None, str | bytes | None]: ...
|
|
46
|
+
|
|
47
|
+
def poll(self) -> int | None: ...
|
|
48
|
+
|
|
49
|
+
def kill(self) -> None: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _decoded_text(stream_payload: str | bytes | None) -> str:
|
|
53
|
+
return decode_stream_text(
|
|
54
|
+
stream_payload, encoding=UTF8_ENCODING, errors=UTF8_DECODE_ERRORS
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _launch_process(
|
|
59
|
+
process_factory: Callable[..., WorkerProcess],
|
|
60
|
+
all_arguments: list[str],
|
|
61
|
+
cwd: Path,
|
|
62
|
+
all_child_environment_variables: dict[str, str],
|
|
63
|
+
) -> WorkerProcess | None:
|
|
64
|
+
try:
|
|
65
|
+
return process_factory(
|
|
66
|
+
all_arguments,
|
|
67
|
+
cwd=str(cwd),
|
|
68
|
+
env=all_child_environment_variables,
|
|
69
|
+
stdin=subprocess.PIPE,
|
|
70
|
+
stdout=subprocess.PIPE,
|
|
71
|
+
stderr=subprocess.PIPE,
|
|
72
|
+
text=True,
|
|
73
|
+
encoding=UTF8_ENCODING,
|
|
74
|
+
errors=UTF8_DECODE_ERRORS,
|
|
75
|
+
start_new_session=_should_start_new_session(),
|
|
76
|
+
creationflags=hidden_window_creation_flags(),
|
|
77
|
+
)
|
|
78
|
+
except OSError:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _drain_attempt_outcome(process: WorkerProcess) -> tuple[bool, str]:
|
|
83
|
+
try:
|
|
84
|
+
captured_stdout, _ = process.communicate(timeout=DRAIN_GRACE_TIMEOUT_SECONDS)
|
|
85
|
+
except subprocess.TimeoutExpired as timeout_error:
|
|
86
|
+
return False, _decoded_text(timeout_error.output)
|
|
87
|
+
return True, _decoded_text(captured_stdout)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _drain_after_termination(
|
|
91
|
+
process: WorkerProcess,
|
|
92
|
+
process_terminator: Callable[[WorkerProcess], None],
|
|
93
|
+
initial_stdout_text: str,
|
|
94
|
+
) -> str:
|
|
95
|
+
latest_stdout_text = initial_stdout_text
|
|
96
|
+
for _ in range(DRAIN_ATTEMPT_LIMIT):
|
|
97
|
+
process_terminator(process)
|
|
98
|
+
did_drain, attempt_stdout_text = _drain_attempt_outcome(process)
|
|
99
|
+
if attempt_stdout_text:
|
|
100
|
+
latest_stdout_text = attempt_stdout_text
|
|
101
|
+
if did_drain:
|
|
102
|
+
return latest_stdout_text
|
|
103
|
+
return latest_stdout_text
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ProcessControls(Protocol):
|
|
107
|
+
"""The process factory, terminator, and clock a worker run uses."""
|
|
108
|
+
|
|
109
|
+
process_factory: Callable[..., WorkerProcess]
|
|
110
|
+
process_terminator: Callable[[WorkerProcess], None]
|
|
111
|
+
monotonic_clock: Callable[[], float]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _await_completion(
|
|
115
|
+
process: WorkerProcess | None,
|
|
116
|
+
prompt_text: str,
|
|
117
|
+
timeout_minutes: int,
|
|
118
|
+
process_terminator: Callable[[WorkerProcess], None],
|
|
119
|
+
) -> tuple[int, str]:
|
|
120
|
+
if process is None:
|
|
121
|
+
return LAUNCH_FAILURE_EXIT_CODE, ""
|
|
122
|
+
try:
|
|
123
|
+
captured_stdout, _ = process.communicate(
|
|
124
|
+
input=prompt_text,
|
|
125
|
+
timeout=timeout_minutes * SECONDS_PER_MINUTE,
|
|
126
|
+
)
|
|
127
|
+
except subprocess.TimeoutExpired as timeout_error:
|
|
128
|
+
stdout_text = _drain_after_termination(
|
|
129
|
+
process, process_terminator, _decoded_text(timeout_error.output)
|
|
130
|
+
)
|
|
131
|
+
return TIMEOUT_EXIT_CODE, stdout_text
|
|
132
|
+
process_exit_code = (
|
|
133
|
+
process.returncode
|
|
134
|
+
if process.returncode is not None
|
|
135
|
+
else LAUNCH_FAILURE_EXIT_CODE
|
|
136
|
+
)
|
|
137
|
+
return process_exit_code, _decoded_text(captured_stdout)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def invoke_worker(
|
|
141
|
+
*,
|
|
142
|
+
all_arguments: list[str],
|
|
143
|
+
cwd: Path,
|
|
144
|
+
all_child_environment_variables: dict[str, str],
|
|
145
|
+
prompt_text: str,
|
|
146
|
+
timeout_minutes: int,
|
|
147
|
+
controls: ProcessControls,
|
|
148
|
+
) -> tuple[int, float, str]:
|
|
149
|
+
"""Launch the worker process and return its exit code, duration, and stdout.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
all_arguments: The full command line, executable first.
|
|
153
|
+
cwd: Working directory the child process runs in.
|
|
154
|
+
all_child_environment_variables: Environment passed to the child.
|
|
155
|
+
prompt_text: The prompt piped to the child on stdin.
|
|
156
|
+
timeout_minutes: Minutes to wait before the child is timed out.
|
|
157
|
+
controls: The process factory, terminator, and clock.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The exit code, the duration in seconds, and the decoded stdout.
|
|
161
|
+
"""
|
|
162
|
+
process_start = controls.monotonic_clock()
|
|
163
|
+
process = _launch_process(
|
|
164
|
+
controls.process_factory, all_arguments, cwd, all_child_environment_variables
|
|
165
|
+
)
|
|
166
|
+
exit_code, stdout_text = _await_completion(
|
|
167
|
+
process, prompt_text, timeout_minutes, controls.process_terminator
|
|
168
|
+
)
|
|
169
|
+
return exit_code, controls.monotonic_clock() - process_start, stdout_text
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Build and write a headless Claude worker's JSON report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
10
|
+
DURATION_DECIMAL_PLACES,
|
|
11
|
+
JSON_IS_ERROR_KEY,
|
|
12
|
+
JSON_RESULT_KEY,
|
|
13
|
+
REPORT_ACCOUNT_KEY,
|
|
14
|
+
REPORT_DURATION_SECONDS_KEY,
|
|
15
|
+
REPORT_EXIT_CODE_KEY,
|
|
16
|
+
REPORT_INDENT,
|
|
17
|
+
REPORT_IS_ERROR_KEY,
|
|
18
|
+
REPORT_REASON_KEY,
|
|
19
|
+
REPORT_RESULT_KEY,
|
|
20
|
+
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
21
|
+
SUMMARY_LINE_TEMPLATE,
|
|
22
|
+
UTF8_ENCODING,
|
|
23
|
+
WAIT_DURATION_SECONDS,
|
|
24
|
+
WAIT_EXIT_CODE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class WorkerReport:
|
|
30
|
+
account: str
|
|
31
|
+
reason: str
|
|
32
|
+
exit_code: int
|
|
33
|
+
duration_seconds: float
|
|
34
|
+
payload: object
|
|
35
|
+
is_error: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bounded_stdout_tail(stdout_text: str) -> str:
|
|
39
|
+
return stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _extract_payload(stdout_text: str) -> tuple[object, bool]:
|
|
43
|
+
try:
|
|
44
|
+
parsed_stdout = json.loads(stdout_text)
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
return _bounded_stdout_tail(stdout_text), True
|
|
47
|
+
if not isinstance(parsed_stdout, dict) or JSON_RESULT_KEY not in parsed_stdout:
|
|
48
|
+
return None, True
|
|
49
|
+
return parsed_stdout[JSON_RESULT_KEY], parsed_stdout.get(JSON_IS_ERROR_KEY) is True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def wait_report(account: str, reason: str) -> WorkerReport:
|
|
53
|
+
"""Build the report for an account decision that says to wait.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
account: The chosen account slot.
|
|
57
|
+
reason: Why the picker chose to wait.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A report carrying the wait exit code and no payload.
|
|
61
|
+
"""
|
|
62
|
+
return WorkerReport(
|
|
63
|
+
account=account,
|
|
64
|
+
reason=reason,
|
|
65
|
+
exit_code=WAIT_EXIT_CODE,
|
|
66
|
+
duration_seconds=WAIT_DURATION_SECONDS,
|
|
67
|
+
payload=None,
|
|
68
|
+
is_error=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def make_report(
|
|
73
|
+
account: str,
|
|
74
|
+
reason: str,
|
|
75
|
+
*,
|
|
76
|
+
exit_code: int,
|
|
77
|
+
duration_seconds: float,
|
|
78
|
+
stdout_text: str,
|
|
79
|
+
) -> WorkerReport:
|
|
80
|
+
"""Build the report for a worker that ran, from its captured stdout.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
account: The chosen account slot.
|
|
84
|
+
reason: Why the picker chose that account.
|
|
85
|
+
exit_code: The worker process's exit code.
|
|
86
|
+
duration_seconds: How long the worker ran.
|
|
87
|
+
stdout_text: The worker's captured stdout text.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A report carrying the parsed payload, or the stdout tail when the
|
|
91
|
+
payload does not parse as the expected JSON shape.
|
|
92
|
+
"""
|
|
93
|
+
payload, has_payload_error = _extract_payload(stdout_text)
|
|
94
|
+
return WorkerReport(
|
|
95
|
+
account=account,
|
|
96
|
+
reason=reason,
|
|
97
|
+
exit_code=exit_code,
|
|
98
|
+
duration_seconds=round(duration_seconds, DURATION_DECIMAL_PLACES),
|
|
99
|
+
payload=payload,
|
|
100
|
+
is_error=(exit_code != 0 or has_payload_error),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def pre_launch_failure_report(
|
|
105
|
+
account: str, reason: str, exit_code: int
|
|
106
|
+
) -> WorkerReport:
|
|
107
|
+
"""Build the report for a failure that happened before the worker launched.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
account: The chosen account slot.
|
|
111
|
+
reason: Why the picker chose that account.
|
|
112
|
+
exit_code: The failure's exit code.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A report with no payload and a zero duration.
|
|
116
|
+
"""
|
|
117
|
+
return make_report(
|
|
118
|
+
account,
|
|
119
|
+
reason,
|
|
120
|
+
exit_code=exit_code,
|
|
121
|
+
duration_seconds=WAIT_DURATION_SECONDS,
|
|
122
|
+
stdout_text="",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def write_report(report_file: Path, report: WorkerReport) -> None:
|
|
127
|
+
"""Write a worker report to disk as indented JSON.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
report_file: Where the JSON report is written.
|
|
131
|
+
report: The report to serialize.
|
|
132
|
+
"""
|
|
133
|
+
report_file.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
report_payload = {
|
|
135
|
+
REPORT_ACCOUNT_KEY: report.account,
|
|
136
|
+
REPORT_REASON_KEY: report.reason,
|
|
137
|
+
REPORT_EXIT_CODE_KEY: report.exit_code,
|
|
138
|
+
REPORT_DURATION_SECONDS_KEY: report.duration_seconds,
|
|
139
|
+
REPORT_RESULT_KEY: report.payload,
|
|
140
|
+
REPORT_IS_ERROR_KEY: report.is_error,
|
|
141
|
+
}
|
|
142
|
+
report_file.write_text(
|
|
143
|
+
json.dumps(report_payload, ensure_ascii=False, indent=REPORT_INDENT) + "\n",
|
|
144
|
+
encoding=UTF8_ENCODING,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _print_summary(report: WorkerReport, report_file: Path) -> None:
|
|
149
|
+
print(
|
|
150
|
+
SUMMARY_LINE_TEMPLATE.format(
|
|
151
|
+
account=report.account,
|
|
152
|
+
exit_code=report.exit_code,
|
|
153
|
+
report_file=report_file,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def finalize_report(report_file: Path, report: WorkerReport) -> int:
|
|
159
|
+
"""Write a worker report to disk and print its one-line summary.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
report_file: Where the JSON report is written.
|
|
163
|
+
report: The report to write and summarize.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The report's exit code.
|
|
167
|
+
"""
|
|
168
|
+
write_report(report_file, report)
|
|
169
|
+
_print_summary(report, report_file)
|
|
170
|
+
return report.exit_code
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Named values for the second-account Claude worker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dev_env_scripts_constants.claude_account_constants import (
|
|
6
|
+
CHOICE_MAIN,
|
|
7
|
+
CHOICE_SECOND,
|
|
8
|
+
CHOICE_WAIT,
|
|
9
|
+
CREDENTIALS_FILE_NAME,
|
|
10
|
+
JSON_ACCOUNT_KEY,
|
|
11
|
+
JSON_CONFIG_DIRECTORY_KEY,
|
|
12
|
+
JSON_REASON_KEY,
|
|
13
|
+
MAIN_CLAUDE_HOME_DIRECTORY_NAME,
|
|
14
|
+
)
|
|
15
|
+
from dev_env_scripts_constants.shared_tree_constants import CLAUDE_CONFIG_DIR_ENV_VAR
|
|
16
|
+
|
|
17
|
+
CLAUDE_BINARY_NAME: str = "claude"
|
|
18
|
+
|
|
19
|
+
DEFAULT_PERMISSION_MODE: str = "auto"
|
|
20
|
+
|
|
21
|
+
DEFAULT_TIMEOUT_MINUTES: int = 60
|
|
22
|
+
|
|
23
|
+
MINIMUM_TIMEOUT_MINUTES: int = 1
|
|
24
|
+
|
|
25
|
+
WAIT_EXIT_CODE: int = 3
|
|
26
|
+
|
|
27
|
+
TIMEOUT_EXIT_CODE: int = 124
|
|
28
|
+
|
|
29
|
+
MISSING_BINARY_EXIT_CODE: int = 127
|
|
30
|
+
|
|
31
|
+
LAUNCH_FAILURE_EXIT_CODE: int = 1
|
|
32
|
+
|
|
33
|
+
WAIT_DURATION_SECONDS: float = 0.0
|
|
34
|
+
|
|
35
|
+
STDOUT_TAIL_CHARACTER_LIMIT: int = 4000
|
|
36
|
+
|
|
37
|
+
DRAIN_GRACE_TIMEOUT_SECONDS: int = 10
|
|
38
|
+
|
|
39
|
+
DRAIN_ATTEMPT_LIMIT: int = 2
|
|
40
|
+
|
|
41
|
+
DURATION_DECIMAL_PLACES: int = 3
|
|
42
|
+
|
|
43
|
+
REPORT_INDENT: int = 2
|
|
44
|
+
|
|
45
|
+
UTF8_ENCODING: str = "utf-8"
|
|
46
|
+
|
|
47
|
+
UTF8_DECODE_ERRORS: str = "replace"
|
|
48
|
+
|
|
49
|
+
CLI_DESCRIPTION: str = "Run one Claude worker using the account picker."
|
|
50
|
+
|
|
51
|
+
PROMPT_FILE_FLAG: str = "--prompt-file"
|
|
52
|
+
|
|
53
|
+
CWD_FLAG: str = "--cwd"
|
|
54
|
+
|
|
55
|
+
REPORT_FILE_FLAG: str = "--report-file"
|
|
56
|
+
|
|
57
|
+
MODEL_FLAG: str = "--model"
|
|
58
|
+
|
|
59
|
+
PERMISSION_MODE_FLAG: str = "--permission-mode"
|
|
60
|
+
|
|
61
|
+
TIMEOUT_MINUTES_FLAG: str = "--timeout-minutes"
|
|
62
|
+
|
|
63
|
+
OUTPUT_FORMAT_FLAG: str = "--output-format"
|
|
64
|
+
|
|
65
|
+
OUTPUT_FORMAT_JSON: str = "json"
|
|
66
|
+
|
|
67
|
+
SINGLE_PROMPT_FLAG: str = "-p"
|
|
68
|
+
|
|
69
|
+
SECONDS_PER_MINUTE: int = 60
|
|
70
|
+
|
|
71
|
+
JSON_RESULT_KEY: str = "result"
|
|
72
|
+
|
|
73
|
+
JSON_IS_ERROR_KEY: str = "is_error"
|
|
74
|
+
|
|
75
|
+
REPORT_ACCOUNT_KEY: str = JSON_ACCOUNT_KEY
|
|
76
|
+
|
|
77
|
+
REPORT_REASON_KEY: str = JSON_REASON_KEY
|
|
78
|
+
|
|
79
|
+
REPORT_EXIT_CODE_KEY: str = "exit_code"
|
|
80
|
+
|
|
81
|
+
REPORT_DURATION_SECONDS_KEY: str = "duration_seconds"
|
|
82
|
+
|
|
83
|
+
REPORT_RESULT_KEY: str = JSON_RESULT_KEY
|
|
84
|
+
|
|
85
|
+
REPORT_IS_ERROR_KEY: str = JSON_IS_ERROR_KEY
|
|
86
|
+
|
|
87
|
+
SUMMARY_LINE_TEMPLATE: str = "account={account} exit_code={exit_code} report={report_file}"
|
|
88
|
+
|
|
89
|
+
INVALID_TIMEOUT_MESSAGE: str = "--timeout-minutes must be at least 1"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"CHOICE_MAIN",
|
|
94
|
+
"CHOICE_SECOND",
|
|
95
|
+
"CHOICE_WAIT",
|
|
96
|
+
"CLAUDE_BINARY_NAME",
|
|
97
|
+
"CLAUDE_CONFIG_DIR_ENV_VAR",
|
|
98
|
+
"CLI_DESCRIPTION",
|
|
99
|
+
"CREDENTIALS_FILE_NAME",
|
|
100
|
+
"CWD_FLAG",
|
|
101
|
+
"DEFAULT_PERMISSION_MODE",
|
|
102
|
+
"DEFAULT_TIMEOUT_MINUTES",
|
|
103
|
+
"DRAIN_ATTEMPT_LIMIT",
|
|
104
|
+
"DRAIN_GRACE_TIMEOUT_SECONDS",
|
|
105
|
+
"DURATION_DECIMAL_PLACES",
|
|
106
|
+
"INVALID_TIMEOUT_MESSAGE",
|
|
107
|
+
"JSON_ACCOUNT_KEY",
|
|
108
|
+
"JSON_CONFIG_DIRECTORY_KEY",
|
|
109
|
+
"JSON_IS_ERROR_KEY",
|
|
110
|
+
"JSON_REASON_KEY",
|
|
111
|
+
"JSON_RESULT_KEY",
|
|
112
|
+
"LAUNCH_FAILURE_EXIT_CODE",
|
|
113
|
+
"MAIN_CLAUDE_HOME_DIRECTORY_NAME",
|
|
114
|
+
"MISSING_BINARY_EXIT_CODE",
|
|
115
|
+
"MINIMUM_TIMEOUT_MINUTES",
|
|
116
|
+
"MODEL_FLAG",
|
|
117
|
+
"OUTPUT_FORMAT_FLAG",
|
|
118
|
+
"OUTPUT_FORMAT_JSON",
|
|
119
|
+
"PERMISSION_MODE_FLAG",
|
|
120
|
+
"PROMPT_FILE_FLAG",
|
|
121
|
+
"REPORT_ACCOUNT_KEY",
|
|
122
|
+
"REPORT_DURATION_SECONDS_KEY",
|
|
123
|
+
"REPORT_EXIT_CODE_KEY",
|
|
124
|
+
"REPORT_FILE_FLAG",
|
|
125
|
+
"REPORT_INDENT",
|
|
126
|
+
"REPORT_IS_ERROR_KEY",
|
|
127
|
+
"REPORT_REASON_KEY",
|
|
128
|
+
"REPORT_RESULT_KEY",
|
|
129
|
+
"SECONDS_PER_MINUTE",
|
|
130
|
+
"SINGLE_PROMPT_FLAG",
|
|
131
|
+
"STDOUT_TAIL_CHARACTER_LIMIT",
|
|
132
|
+
"SUMMARY_LINE_TEMPLATE",
|
|
133
|
+
"TIMEOUT_EXIT_CODE",
|
|
134
|
+
"TIMEOUT_MINUTES_FLAG",
|
|
135
|
+
"UTF8_DECODE_ERRORS",
|
|
136
|
+
"UTF8_ENCODING",
|
|
137
|
+
"WAIT_DURATION_SECONDS",
|
|
138
|
+
"WAIT_EXIT_CODE",
|
|
139
|
+
]
|
|
@@ -73,6 +73,7 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
73
73
|
)
|
|
74
74
|
from shared_tree_paths import resolve_shared_process_tree_scripts_directory
|
|
75
75
|
from grok_worker_preflight import poteto_mode_skill_path
|
|
76
|
+
from subprocess_stream_text import decode_stream_text
|
|
76
77
|
|
|
77
78
|
_shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
|
|
78
79
|
__file__,
|
|
@@ -200,11 +201,7 @@ def _classify_completion(
|
|
|
200
201
|
|
|
201
202
|
|
|
202
203
|
def _normalize_stream(stream_payload: str | bytes | None) -> str:
|
|
203
|
-
|
|
204
|
-
return ""
|
|
205
|
-
if isinstance(stream_payload, bytes):
|
|
206
|
-
return stream_payload.decode(UTF8_ENCODING, errors=UTF8_DECODE_ERRORS)
|
|
207
|
-
return stream_payload
|
|
204
|
+
return decode_stream_text(stream_payload, encoding=UTF8_ENCODING, errors=UTF8_DECODE_ERRORS)
|
|
208
205
|
|
|
209
206
|
|
|
210
207
|
def _resolve_returncode(process: subprocess.Popen[str]) -> int:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Decode a subprocess stream text payload captured in text or bytes mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def decode_stream_text(
|
|
7
|
+
stream_payload: str | bytes | None,
|
|
8
|
+
*,
|
|
9
|
+
encoding: str,
|
|
10
|
+
errors: str,
|
|
11
|
+
) -> str:
|
|
12
|
+
"""Decode a captured subprocess stream text payload, whichever mode captured it.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
stream_payload: The captured stream content, or None when nothing
|
|
16
|
+
was captured.
|
|
17
|
+
encoding: The text encoding to use when the payload is bytes.
|
|
18
|
+
errors: The decode error handler to use when the payload is bytes.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The decoded text, an empty string when stream_payload is None, or
|
|
22
|
+
stream_payload unchanged when it is already text.
|
|
23
|
+
"""
|
|
24
|
+
if stream_payload is None:
|
|
25
|
+
return ""
|
|
26
|
+
if isinstance(stream_payload, bytes):
|
|
27
|
+
return stream_payload.decode(encoding, errors=errors)
|
|
28
|
+
return stream_payload
|