claude-dev-env 8.43.5 → 8.43.6

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 (32) hide show
  1. package/_shared/advisor/advisor-protocol.md +2 -2
  2. package/_shared/advisor/reference/cli-chain.md +9 -12
  3. package/_shared/advisor/reference/lifecycle.md +1 -1
  4. package/_shared/advisor/reference/third-party-bind.md +3 -3
  5. package/bin/install.test.mjs +1 -1
  6. package/docs/second-claude-account.md +11 -13
  7. package/package.json +1 -1
  8. package/scripts/_code_review_test_support.py +29 -39
  9. package/scripts/claude_account_worker.py +46 -297
  10. package/scripts/claude_account_worker_process.py +18 -154
  11. package/scripts/claude_account_worker_report.py +8 -4
  12. package/scripts/claude_chain_usage.py +7 -271
  13. package/scripts/codec_forwarding_test_support.py +11 -65
  14. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
  15. package/scripts/invoke_code_review.py +34 -56
  16. package/scripts/resolve_worker_spawn.py +40 -72
  17. package/scripts/test_account_broker_guard.py +0 -4
  18. package/scripts/test_claude_account_worker.py +105 -350
  19. package/scripts/test_claude_chain_usage.py +50 -510
  20. package/scripts/test_dispatcher_profile_import.py +5 -9
  21. package/scripts/test_invoke_code_review.py +7 -6
  22. package/scripts/test_invoke_code_review_chain.py +26 -0
  23. package/scripts/test_invoke_code_review_cli.py +8 -8
  24. package/scripts/test_invoke_code_review_codec.py +3 -11
  25. package/scripts/test_invoke_code_review_contract.py +5 -1
  26. package/scripts/test_resolve_worker_spawn.py +118 -265
  27. package/scripts/test_resolve_worker_spawn_codec.py +9 -21
  28. package/scripts/tests/test_code_review_constants.py +2 -2
  29. package/scripts/claude_account_choice.py +0 -422
  30. package/scripts/claude_chain_runner.py +0 -1289
  31. package/scripts/test_claude_account_choice.py +0 -355
  32. package/scripts/test_claude_chain_runner.py +0 -1839
@@ -7,14 +7,14 @@ Mode decision::
7
7
  host=Claude, session_model=sonnet -> mode chain (headless opus spawn)
8
8
  host=ThirdParty, any model -> mode chain
9
9
 
10
- Chain mode runs ``run_claude`` with argv from ``build_code_review_arguments``
10
+ Chain mode runs the account broker with argv from ``build_code_review_arguments``
11
11
  (single-turn prompt, model opus, json output, and the permission mode this
12
12
  caller is allowed to ask the review binary for).
13
13
 
14
14
  cwd is the PR working tree and stdin is redirected from the empty stream so
15
15
  the spawn does not wait for interactive input. Result JSON on stdout only::
16
16
 
17
- {"mode", "served_command", "returncode", "dirty_tree"}
17
+ {"mode", "served_command", "returncode", "dirty_tree", "status", "wait_reset_at"}
18
18
 
19
19
  Import ``invoke_code_review`` for the outcome object, or run as a CLI::
20
20
 
@@ -29,7 +29,7 @@ import json
29
29
  import os
30
30
  import subprocess
31
31
  import sys
32
- from collections.abc import Callable, Sequence
32
+ from collections.abc import Callable
33
33
  from dataclasses import dataclass
34
34
  from pathlib import Path
35
35
 
@@ -58,16 +58,13 @@ from tier_model_ids import detect_host_profile # noqa: E402
58
58
  from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
59
59
  HOST_PROFILE_CLAUDE,
60
60
  )
61
- import claude_chain_runner as chain_runner # noqa: E402
62
- from claude_chain_runner import ( # noqa: E402
63
- ChainConfigurationError,
64
- ChainInvocationOutcome,
65
- run_claude,
66
- )
67
- from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
68
- CHAIN_CONFIG_ERROR_EXIT_CODE,
69
- collect_forwarded_text_codec,
61
+ from account_broker import run_job
62
+ from dev_env_scripts_constants.account_broker_constants import (
63
+ BrokerConfigurationError,
64
+ JobOutcome,
65
+ Product,
70
66
  )
67
+ from dev_env_scripts_constants.claude_chain_constants import CHAIN_CONFIG_ERROR_EXIT_CODE
71
68
  from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
72
69
  ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER,
73
70
  CLI_EFFORT_HELP,
@@ -116,7 +113,7 @@ class CodeReviewOutcome:
116
113
 
117
114
  ``mode`` is ``in_session`` when the skill should run the slash command
118
115
  itself, or ``chain`` when a headless spawn already ran. ``served_command``
119
- names the chain binary that served a chain run, or ``None`` otherwise.
116
+ names the broker account that served a chain run, or ``None`` otherwise.
120
117
  ``is_dirty_tree`` is True when ``git status --porcelain`` is non-empty
121
118
  after a chain run (fixes applied).
122
119
  """
@@ -125,9 +122,11 @@ class CodeReviewOutcome:
125
122
  served_command: str | None
126
123
  returncode: int
127
124
  is_dirty_tree: bool
125
+ status: str | None = None
126
+ wait_reset_at: str | None = None
128
127
 
129
128
 
130
- review_claude_runner = run_claude
129
+ review_claude_runner = run_job
131
130
  review_host_profile_detector = detect_host_profile
132
131
  review_git_status_runner = subprocess.run
133
132
 
@@ -198,7 +197,7 @@ def build_code_review_prompt(effort: str) -> str:
198
197
  def build_code_review_arguments(
199
198
  effort: str = DEFAULT_CODE_REVIEW_EFFORT,
200
199
  ) -> list[str]:
201
- """Return the argv tokens passed to ``run_claude`` for a chain review.
200
+ """Return the argv tokens passed to the account broker for a review.
202
201
 
203
202
  ::
204
203
 
@@ -308,41 +307,16 @@ def _run_claude_with_empty_stdin(
308
307
  *,
309
308
  timeout_seconds: int,
310
309
  working_directory: Path,
311
- ) -> ChainInvocationOutcome:
312
- working_directory_path = str(working_directory)
313
-
314
- def _runner_with_empty_stdin(
315
- all_invocation_tokens: Sequence[str],
316
- *all_positionals: object,
317
- **all_keywords: object,
318
- ) -> subprocess.CompletedProcess[str]:
319
- del all_positionals
320
- maybe_timeout = all_keywords.get("timeout")
321
- timeout_for_run: float | None
322
- if isinstance(maybe_timeout, (int, float)):
323
- timeout_for_run = float(maybe_timeout)
324
- else:
325
- timeout_for_run = None
326
- forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
327
- completed_process: subprocess.CompletedProcess[str] = previous_runner(
328
- all_invocation_tokens,
329
- capture_output=True,
330
- text=True,
331
- timeout=timeout_for_run,
332
- check=False,
333
- stdin=subprocess.DEVNULL,
334
- cwd=working_directory_path,
335
- **forwarded_text_codec,
336
- )
337
- return completed_process
338
-
339
- empty_stdin_runner: TextCapturingSubprocessRunner = _runner_with_empty_stdin
340
- with chain_runner.override_chain_subprocess_runner(
341
- empty_stdin_runner
342
- ) as previous_runner:
343
- return review_claude_runner(
344
- all_claude_arguments, timeout_seconds=timeout_seconds
345
- )
310
+ ) -> JobOutcome:
311
+ return review_claude_runner(
312
+ Product.CLAUDE,
313
+ ["claude", *all_claude_arguments],
314
+ timeout_seconds=timeout_seconds,
315
+ stdin_text="",
316
+ cwd=working_directory,
317
+ encoding="utf-8",
318
+ errors="replace",
319
+ )
346
320
 
347
321
 
348
322
  def _in_session_outcome() -> CodeReviewOutcome:
@@ -355,15 +329,17 @@ def _in_session_outcome() -> CodeReviewOutcome:
355
329
 
356
330
 
357
331
  def _chain_outcome(
358
- chain_outcome: ChainInvocationOutcome,
332
+ chain_outcome: JobOutcome,
359
333
  *,
360
334
  working_directory: Path,
361
335
  ) -> CodeReviewOutcome:
362
336
  return CodeReviewOutcome(
363
337
  mode=MODE_CHAIN,
364
- served_command=chain_outcome.served_command,
338
+ served_command=chain_outcome.account_name,
365
339
  returncode=chain_outcome.returncode,
366
- is_dirty_tree=is_working_tree_dirty(working_directory),
340
+ is_dirty_tree=is_working_tree_dirty(working_directory) if chain_outcome.account_name else False,
341
+ status=chain_outcome.status,
342
+ wait_reset_at=chain_outcome.wait_reset_at.isoformat() if chain_outcome.wait_reset_at else None,
367
343
  )
368
344
 
369
345
 
@@ -410,7 +386,7 @@ def invoke_code_review(
410
386
  Args:
411
387
  working_directory: PR working tree used as cwd for the chain spawn.
412
388
  session_model: Caller-stated session model short alias.
413
- timeout_seconds: Timeout applied to each chain binary invocation.
389
+ timeout_seconds: Timeout applied to the broker job.
414
390
  effort: Effort token embedded in the ``/code-review`` prompt.
415
391
 
416
392
  Returns:
@@ -447,6 +423,8 @@ def encode_code_review_outcome(
447
423
  RESULT_KEY_SERVED_COMMAND: review_outcome.served_command,
448
424
  RESULT_KEY_RETURNCODE: review_outcome.returncode,
449
425
  RESULT_KEY_DIRTY_TREE: review_outcome.is_dirty_tree,
426
+ "status": review_outcome.status,
427
+ "wait_reset_at": review_outcome.wait_reset_at,
450
428
  }
451
429
 
452
430
 
@@ -469,7 +447,7 @@ def _add_review_arguments(parser: argparse.ArgumentParser) -> None:
469
447
  dest="timeout_seconds",
470
448
  type=int,
471
449
  default=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
472
- help="Timeout in seconds applied to each chain binary invocation.",
450
+ help="Timeout in seconds applied to the broker job.",
473
451
  )
474
452
 
475
453
 
@@ -509,7 +487,7 @@ def _run_plain_review_cli(*, parsed_arguments: argparse.Namespace, effort: str)
509
487
  timeout_seconds=parsed_arguments.timeout_seconds,
510
488
  effort=effort,
511
489
  )
512
- except ChainConfigurationError:
490
+ except BrokerConfigurationError:
513
491
  review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
514
492
  except ValueError:
515
493
  review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
@@ -9,7 +9,7 @@ Walk order:
9
9
  On a Claude host with the claude tier disabled, stop with
10
10
  ``claude_agent_required`` so the calling skill runs the Agent tool.
11
11
  On a third-party host, or when the claude tier is enabled, tier 3 runs
12
- ``claude_chain_runner.run_claude`` with ``-p --output-format json --agent
12
+ the account broker with ``-p --output-format json --agent
13
13
  <stem>``, prompt body on stdin from the prompt file, and subprocess
14
14
  ``cwd`` set to the caller's working directory.
15
15
 
@@ -27,10 +27,9 @@ import json
27
27
  import os
28
28
  import subprocess
29
29
  import sys
30
- from collections.abc import Callable, Sequence
30
+ from collections.abc import Callable
31
31
  from dataclasses import dataclass
32
32
  from pathlib import Path
33
- from typing import IO
34
33
 
35
34
  _scripts_directory_path = Path(__file__).resolve().parent
36
35
  _scripts_directory = str(_scripts_directory_path)
@@ -57,14 +56,12 @@ from tier_model_ids import detect_host_profile # noqa: E402
57
56
  from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
58
57
  HOST_PROFILE_CLAUDE,
59
58
  )
60
- import claude_chain_runner as chain_runner # noqa: E402
61
- from claude_chain_runner import ( # noqa: E402
62
- ChainConfigurationError,
63
- ChainInvocationOutcome,
64
- run_claude,
65
- )
66
- from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
67
- collect_forwarded_text_codec,
59
+ from account_broker import run_job
60
+ from dev_env_scripts_constants.account_broker_constants import (
61
+ BrokerConfigurationError,
62
+ JobOutcome,
63
+ Product,
64
+ WAIT_EXIT_CODE,
68
65
  )
69
66
  from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
70
67
  AGENT_FLAG,
@@ -136,20 +133,16 @@ class SpawnOutcome:
136
133
  captured_stdout: str
137
134
  returncode: int
138
135
  is_config_error: bool = False
136
+ status: str | None = None
137
+ wait_reset_at: str | None = None
139
138
 
140
139
 
141
140
  spawn_preflight_runner = run_preflight
142
141
  spawn_grok_runner = run_headless_worker
143
- spawn_claude_runner = run_claude
142
+ spawn_claude_runner = run_job
144
143
  spawn_host_profile_detector = detect_host_profile
145
144
 
146
145
 
147
- TextCapturingSubprocessRunner = Callable[
148
- ...,
149
- subprocess.CompletedProcess[str],
150
- ]
151
-
152
-
153
146
  def _attempt(tier: int, *, is_ok: bool, reason: str | None) -> SpawnAttempt:
154
147
  return SpawnAttempt(tier=tier, is_ok=is_ok, reason=reason)
155
148
 
@@ -173,51 +166,22 @@ def _build_claude_arguments(*, agent_name: str) -> list[str]:
173
166
  ]
174
167
 
175
168
 
176
- def _timeout_seconds_from_keywords(
177
- all_keywords: dict[str, object],
178
- ) -> float | None:
179
- maybe_timeout = all_keywords.get("timeout")
180
- if isinstance(maybe_timeout, (int, float)):
181
- return float(maybe_timeout)
182
- return None
183
-
184
-
185
169
  def _run_claude_with_headless_overrides(
186
170
  all_claude_arguments: list[str],
187
171
  *,
188
172
  timeout_seconds: int,
189
173
  working_directory: Path,
190
- prompt_stdin: IO[str],
191
- ) -> ChainInvocationOutcome:
192
- working_directory_path = str(working_directory)
193
-
194
- def _runner_with_headless_overrides(
195
- all_invocation_tokens: Sequence[str],
196
- *all_positionals: object,
197
- **all_keywords: object,
198
- ) -> subprocess.CompletedProcess[str]:
199
- del all_positionals
200
- prompt_stdin.seek(0)
201
- forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
202
- completed_process: subprocess.CompletedProcess[str] = previous_runner(
203
- all_invocation_tokens,
204
- capture_output=True,
205
- text=True,
206
- timeout=_timeout_seconds_from_keywords(all_keywords),
207
- check=False,
208
- stdin=prompt_stdin,
209
- cwd=working_directory_path,
210
- **forwarded_text_codec,
211
- )
212
- return completed_process
213
-
214
- headless_runner: TextCapturingSubprocessRunner = _runner_with_headless_overrides
215
- with chain_runner.override_chain_subprocess_runner(
216
- headless_runner
217
- ) as previous_runner:
218
- return spawn_claude_runner(
219
- all_claude_arguments, timeout_seconds=timeout_seconds
220
- )
174
+ prompt_text: str,
175
+ ) -> JobOutcome:
176
+ return spawn_claude_runner(
177
+ Product.CLAUDE,
178
+ ["claude", *all_claude_arguments],
179
+ timeout_seconds=timeout_seconds,
180
+ stdin_text=prompt_text,
181
+ cwd=working_directory,
182
+ encoding="utf-8",
183
+ errors="replace",
184
+ )
221
185
 
222
186
 
223
187
  def _prompt_file_unreadable_outcome(
@@ -261,9 +225,9 @@ def _claude_agent_required_outcome(
261
225
 
262
226
  def _served_claude_outcome(
263
227
  all_attempts: list[SpawnAttempt],
264
- chain_outcome: ChainInvocationOutcome,
228
+ chain_outcome: JobOutcome,
265
229
  ) -> SpawnOutcome:
266
- is_served = chain_outcome.served_command is not None
230
+ is_served = chain_outcome.account_name is not None
267
231
  is_ok = is_served and chain_outcome.returncode == SPAWN_SERVED_EXIT_CODE
268
232
  all_attempts.append(_attempt(TIER_CLAUDE_HEADLESS, is_ok=is_ok, reason=None))
269
233
  if not is_served:
@@ -273,6 +237,8 @@ def _served_claude_outcome(
273
237
  all_attempts=tuple(all_attempts),
274
238
  captured_stdout=chain_outcome.stdout,
275
239
  returncode=chain_outcome.returncode,
240
+ status=chain_outcome.status,
241
+ wait_reset_at=chain_outcome.wait_reset_at.isoformat() if chain_outcome.wait_reset_at else None,
276
242
  )
277
243
  return SpawnOutcome(
278
244
  tier_used=TIER_CLAUDE_HEADLESS,
@@ -280,6 +246,7 @@ def _served_claude_outcome(
280
246
  all_attempts=tuple(all_attempts),
281
247
  captured_stdout=chain_outcome.stdout,
282
248
  returncode=chain_outcome.returncode,
249
+ status=chain_outcome.status,
283
250
  )
284
251
 
285
252
 
@@ -293,18 +260,15 @@ def _run_tier_claude_headless(
293
260
  ) -> SpawnOutcome:
294
261
  all_claude_arguments = _build_claude_arguments(agent_name=agent_name)
295
262
  try:
296
- prompt_stdin = prompt_file.open(encoding=UTF8_ENCODING)
263
+ prompt_text = prompt_file.read_text(encoding=UTF8_ENCODING)
297
264
  except OSError:
298
265
  return _prompt_file_unreadable_outcome(all_attempts)
299
- try:
300
- chain_outcome = _run_claude_with_headless_overrides(
301
- all_claude_arguments,
302
- timeout_seconds=timeout_seconds,
303
- working_directory=working_directory,
304
- prompt_stdin=prompt_stdin,
305
- )
306
- finally:
307
- prompt_stdin.close()
266
+ chain_outcome = _run_claude_with_headless_overrides(
267
+ all_claude_arguments,
268
+ timeout_seconds=timeout_seconds,
269
+ working_directory=working_directory,
270
+ prompt_text=prompt_text,
271
+ )
308
272
  return _served_claude_outcome(all_attempts, chain_outcome)
309
273
 
310
274
 
@@ -505,6 +469,8 @@ def encode_spawn_outcome(spawn_outcome: SpawnOutcome) -> dict[str, object]:
505
469
  RESULT_KEY_ATTEMPTS: all_encoded_attempts,
506
470
  RESULT_KEY_OUTPUT: spawn_outcome.captured_stdout,
507
471
  RESULT_KEY_RETURNCODE: spawn_outcome.returncode,
472
+ "status": spawn_outcome.status,
473
+ "wait_reset_at": spawn_outcome.wait_reset_at,
508
474
  }
509
475
 
510
476
 
@@ -568,6 +534,8 @@ def _exit_code_for_outcome(
568
534
  ) -> int:
569
535
  if is_config_error or spawn_outcome.is_config_error:
570
536
  return SPAWN_CONFIG_ERROR_EXIT_CODE
537
+ if spawn_outcome.status in {"wait", "exhausted"}:
538
+ return WAIT_EXIT_CODE
571
539
  if spawn_outcome.tier_used is None:
572
540
  return SPAWN_EXHAUSTED_EXIT_CODE
573
541
  return SPAWN_SERVED_EXIT_CODE
@@ -639,7 +607,7 @@ def main(all_command_arguments: list[str]) -> int:
639
607
 
640
608
  A timeout outside the accepted bounds is a config fault: the run prints a
641
609
  structured outcome whose attempt reason is ``timeout_out_of_bounds`` and
642
- exits ``3``, rather than raising out of the CLI. A chain misconfiguration,
610
+ exits ``3``, rather than raising out of the CLI. A broker configuration error,
643
611
  or an unknown host profile refused by ``detect_host_profile``, prints the
644
612
  same exit code with the refusal text as its output.
645
613
 
@@ -672,7 +640,7 @@ def main(all_command_arguments: list[str]) -> int:
672
640
  except WorkerTimeoutOutOfBoundsError as bounds_error:
673
641
  is_config_error = True
674
642
  spawn_outcome = _timeout_out_of_bounds_outcome(bounds_error)
675
- except (ChainConfigurationError, ValueError) as configuration_error:
643
+ except (BrokerConfigurationError, ValueError) as configuration_error:
676
644
  is_config_error = True
677
645
  spawn_outcome = _config_error_outcome(configuration_error)
678
646
  return _write_spawn_outcome_and_exit_code(
@@ -7,10 +7,6 @@ from pathlib import Path
7
7
  SCRIPTS = Path(__file__).resolve().parent
8
8
  ALLOWED_LEGACY_MODULES = frozenset(
9
9
  {
10
- "claude_account_choice.py",
11
- "claude_account_worker.py",
12
- "claude_chain_runner.py",
13
- "claude_chain_usage.py",
14
10
  "codex_account_choice.py",
15
11
  "codex_account_meters.py",
16
12
  }