claude-dev-env 2.11.0 → 2.12.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.
@@ -5,8 +5,11 @@ Open this when `ADVISOR_SOL_XHIGH` is set and a bind is starting.
5
5
 
6
6
  ## Flag
7
7
 
8
- `ADVISOR_SOL_XHIGH=1` (or `true` / `yes` / `on`) opens the rung, set in the environment or by the consuming skill's invocation.
9
- Flag off: the walk starts at the host's Claude ladder, Fable first.
8
+ `ADVISOR_SOL_XHIGH=1` (or `true` / `yes` / `on`) opens the rung. Two channels exist: set the variable in the helper's process environment, or pass `--enable-sol` on the helper invocation — the CLI flag opens the rung for that run without touching the environment.
9
+ Flag off both ways: the walk starts at the host's Claude ladder, Fable first.
10
+ A Windows `setx` write only updates the persisted user environment; only a process started after that write inherits the new value, so an already-running session either sets the flag in its own invoking process environment or passes `--enable-sol`.
11
+
12
+ Every fallback reply carries a `fallback_kind` field: `declined` when policy closed the rung (flag off, usage meter at or below the gate) and `broken` when the Sol path itself failed (missing executable, spawn error, timeout, malformed reply). A `broken` fallback is a defect to report, not a routing outcome.
10
13
 
11
14
  ## Preflight
12
15
 
@@ -25,6 +25,7 @@ State plainly:
25
25
  - Reply via SendMessage to whoever sent the consult, by name — each reply goes back to its own sender, and many different consumers may reach this one agent.
26
26
  - Treat each consult on its own terms, keyed to the sender's stated assignment. Different consumers' consults will interleave in this one transcript — keep each consumer's context separate, and blend only when a consult explicitly asks for that.
27
27
  - If a consult re-raises a question already answered, with nothing new attached, reply by restating the prior answer and naming it as a restatement.
28
+ - Every reply, including this first bind turn, opens its first line with exactly one of the four uppercase signal words — `ENDORSE`, `CORRECTION`, `PLAN`, `STOP` — and nothing else on that line. Standing by after the bind is itself an `ENDORSE` of the charter, stated as that first line.
28
29
 
29
30
  The agent finishes its first turn standing by. `SendMessage` alone resumes it; between consults it waits quietly.
30
31
 
@@ -7,6 +7,7 @@ import importlib
7
7
  import json
8
8
  import math
9
9
  import os
10
+ import shutil
10
11
  import subprocess
11
12
  import sys
12
13
  from collections.abc import Callable, Mapping, Sequence
@@ -20,6 +21,7 @@ if _config_directory_text not in sys.path:
20
21
  sys.path.insert(0, _config_directory_text)
21
22
 
22
23
  from advisor_scripts_constants.sol_advisor_constants import ( # noqa: E402
24
+ ADVISOR_CODEX_EXECUTABLE_ENV_VAR,
23
25
  CODEX_CONFIG_FLAG,
24
26
  CODEX_EXECUTABLE,
25
27
  CODEX_EXEC_SUBCOMMAND,
@@ -35,6 +37,10 @@ from advisor_scripts_constants.sol_advisor_constants import ( # noqa: E402
35
37
  SOL_CODEX_TIMEOUT_REASON,
36
38
  SOL_CODEX_TIMEOUT_SECONDS,
37
39
  SOL_ENV_VAR,
40
+ SOL_ENABLE_FLAG,
41
+ SOL_EXECUTABLE_NOT_FOUND_REASON,
42
+ SOL_FALLBACK_KIND_BROKEN,
43
+ SOL_FALLBACK_KIND_DECLINED,
38
44
  SOL_INVALID_SIGNAL_REASON,
39
45
  SOL_MALFORMED_JSONL_REASON,
40
46
  SOL_MISSING_SESSION_REASON,
@@ -63,6 +69,7 @@ class SolPreflight:
63
69
  eligible: bool
64
70
  percent_left: float | None
65
71
  reason: str
72
+ fallback_kind: str | None = None
66
73
 
67
74
 
68
75
  @dataclass(frozen=True)
@@ -78,17 +85,21 @@ class CodexSolAdvisorReply:
78
85
  sol_enabled: bool
79
86
  selected_tier: str
80
87
  outcome: str
88
+ fallback_kind: str | None
81
89
 
82
90
 
83
91
  def _preflight_fallback(
84
- reason: str, percent_left: float | None
92
+ reason: str,
93
+ percent_left: float | None,
94
+ fallback_kind: str = SOL_FALLBACK_KIND_BROKEN,
85
95
  ) -> SolPreflight:
86
- return SolPreflight(False, percent_left, reason)
96
+ return SolPreflight(False, percent_left, reason, fallback_kind)
87
97
 
88
98
 
89
99
  def _reply_fallback(
90
100
  reason: str,
91
101
  is_sol_enabled: bool,
102
+ fallback_kind: str | None = SOL_FALLBACK_KIND_BROKEN,
92
103
  ) -> CodexSolAdvisorReply:
93
104
  return CodexSolAdvisorReply(
94
105
  session_id=None,
@@ -100,6 +111,7 @@ def _reply_fallback(
100
111
  sol_enabled=is_sol_enabled,
101
112
  selected_tier=ADVISOR_FALLBACK_TIER,
102
113
  outcome=ADVISOR_FALLBACK_RESULT,
114
+ fallback_kind=fallback_kind,
103
115
  )
104
116
 
105
117
 
@@ -118,9 +130,16 @@ def _reply_success(
118
130
  sol_enabled=True,
119
131
  selected_tier=ADVISOR_MODEL_TIER,
120
132
  outcome=CODEX_BIND_SUCCESS_TOKEN,
133
+ fallback_kind=None,
121
134
  )
122
135
 
123
136
 
137
+ def _resolved_setting_by_name(
138
+ setting_by_name: Mapping[str, str] | None,
139
+ ) -> Mapping[str, str]:
140
+ return os.environ if setting_by_name is None else setting_by_name
141
+
142
+
124
143
  def is_sol_advisor_enabled(
125
144
  setting_by_name: Mapping[str, str] | None,
126
145
  ) -> bool:
@@ -132,7 +151,7 @@ def is_sol_advisor_enabled(
132
151
  Returns:
133
152
  Whether the Sol feature flag contains a recognized truthy value.
134
153
  """
135
- resolved_setting_by_name = os.environ if setting_by_name is None else setting_by_name
154
+ resolved_setting_by_name = _resolved_setting_by_name(setting_by_name)
136
155
  return (
137
156
  resolved_setting_by_name.get(SOL_ENV_VAR, "").strip().lower()
138
157
  in ALL_SOL_TRUTHY_VALUES
@@ -223,11 +242,11 @@ def run_sol_preflight(
223
242
  return _preflight_fallback(
224
243
  f"{SOL_PREFLIGHT_FAILURE_REASON}: usage meter is unknown", None
225
244
  )
226
- usage_gate = _load_usage_gate(probe_path)
227
245
  if not usage_gate(percent_left):
228
246
  return _preflight_fallback(
229
247
  f"{SOL_PREFLIGHT_FAILURE_REASON}: usage meter is at or below the gate",
230
248
  percent_left,
249
+ fallback_kind=SOL_FALLBACK_KIND_DECLINED,
231
250
  )
232
251
  except subprocess.TimeoutExpired as probe_error:
233
252
  return _preflight_fallback(f"{SOL_PROBE_TIMEOUT_REASON}: {probe_error}", None)
@@ -245,17 +264,44 @@ def run_sol_preflight(
245
264
  return SolPreflight(True, percent_left, "usage meter is above the Sol gate")
246
265
 
247
266
 
248
- def build_codex_arguments(session_id: str | None = None) -> list[str]:
267
+ def resolve_codex_executable(
268
+ setting_by_name: Mapping[str, str] | None,
269
+ ) -> str | None:
270
+ """Resolve the Codex CLI executable to an invocable name or path.
271
+
272
+ A bare "codex" name fails Windows `CreateProcess`, since the npm shim
273
+ directory holds only `codex` (a sh script), `codex.cmd`, and `codex.ps1`.
274
+ `shutil.which` finds `codex.cmd` via `PATHEXT`. An explicit override
275
+ always wins and is trusted without a `which` check.
276
+
277
+ Args:
278
+ setting_by_name: Optional environment-like settings mapping.
279
+
280
+ Returns:
281
+ An invocable executable name or path, or None when unresolved.
282
+ """
283
+ resolved_setting_by_name = _resolved_setting_by_name(setting_by_name)
284
+ executable_override = resolved_setting_by_name.get(ADVISOR_CODEX_EXECUTABLE_ENV_VAR, "").strip()
285
+ if executable_override:
286
+ return executable_override
287
+ return shutil.which(CODEX_EXECUTABLE)
288
+
289
+
290
+ def build_codex_arguments(
291
+ codex_executable: str,
292
+ session_id: str | None = None,
293
+ ) -> list[str]:
249
294
  """Build the installed CLI's shell-free bind or resume argv.
250
295
 
251
296
  Args:
297
+ codex_executable: Resolved executable name or path to invoke.
252
298
  session_id: Optional existing session to resume.
253
299
 
254
300
  Returns:
255
301
  The shell-free Codex command argument vector.
256
302
  """
257
303
  command_arguments = [
258
- CODEX_EXECUTABLE,
304
+ codex_executable,
259
305
  CODEX_EXEC_SUBCOMMAND,
260
306
  CODEX_MODEL_FLAG,
261
307
  ADVISOR_CODEX_MODEL_ID,
@@ -333,6 +379,21 @@ def parse_codex_jsonl_reply(
333
379
  return _reply_success(discovered_session_id, final_guidance, guidance_signal)
334
380
 
335
381
 
382
+ def _resolve_sol_preflight(
383
+ preflight: SolPreflight | None,
384
+ probe_path: Path | None,
385
+ process_runner: Callable[..., subprocess.CompletedProcess[str]],
386
+ ) -> SolPreflight:
387
+ if preflight is not None:
388
+ return preflight
389
+ resolved_probe_path = (
390
+ resolve_usage_probe_path(Path.home()) if probe_path is None else probe_path
391
+ )
392
+ return run_sol_preflight(
393
+ probe_path=resolved_probe_path, process_runner=process_runner
394
+ )
395
+
396
+
336
397
  def run_codex_sol_advisor(
337
398
  prompt: str,
338
399
  working_directory: Path,
@@ -356,26 +417,25 @@ def run_codex_sol_advisor(
356
417
  Returns:
357
418
  The parsed Sol guidance or an explicit Fable fallback reply.
358
419
  """
359
- is_sol_enabled = is_sol_advisor_enabled(setting_by_name)
360
- if not is_sol_enabled:
361
- return _reply_fallback("Sol advisor flag is disabled", False)
362
- resolved_preflight = (
363
- run_sol_preflight(
364
- probe_path=(
365
- resolve_usage_probe_path(Path.home())
366
- if probe_path is None
367
- else probe_path
368
- ),
369
- process_runner=process_runner,
420
+ if not is_sol_advisor_enabled(setting_by_name):
421
+ return _reply_fallback(
422
+ "Sol advisor flag is disabled",
423
+ False,
424
+ fallback_kind=SOL_FALLBACK_KIND_DECLINED,
370
425
  )
371
- if preflight is None
372
- else preflight
373
- )
426
+ codex_executable = resolve_codex_executable(setting_by_name)
427
+ if codex_executable is None:
428
+ return _reply_fallback(SOL_EXECUTABLE_NOT_FOUND_REASON, True)
429
+ resolved_preflight = _resolve_sol_preflight(preflight, probe_path, process_runner)
374
430
  if not resolved_preflight.eligible:
375
- return _reply_fallback(resolved_preflight.reason, is_sol_enabled)
431
+ return _reply_fallback(
432
+ resolved_preflight.reason,
433
+ True,
434
+ fallback_kind=resolved_preflight.fallback_kind,
435
+ )
376
436
  try:
377
437
  completed_process = process_runner(
378
- build_codex_arguments(session_id=session_id),
438
+ build_codex_arguments(codex_executable, session_id=session_id),
379
439
  cwd=str(working_directory),
380
440
  input=prompt,
381
441
  capture_output=True,
@@ -385,22 +445,18 @@ def run_codex_sol_advisor(
385
445
  timeout=SOL_CODEX_TIMEOUT_SECONDS,
386
446
  )
387
447
  except subprocess.TimeoutExpired as bind_error:
388
- return _reply_fallback(
389
- f"{SOL_CODEX_TIMEOUT_REASON}: {bind_error}", is_sol_enabled
390
- )
448
+ return _reply_fallback(f"{SOL_CODEX_TIMEOUT_REASON}: {bind_error}", True)
391
449
  except (OSError, subprocess.SubprocessError) as bind_error:
392
- return _reply_fallback(
393
- f"{SOL_BIND_FAILURE_REASON}: {bind_error}", is_sol_enabled
394
- )
450
+ return _reply_fallback(f"{SOL_BIND_FAILURE_REASON}: {bind_error}", True)
395
451
  if completed_process.returncode != 0:
396
452
  return _reply_fallback(
397
453
  f"{SOL_BIND_FAILURE_REASON}: process exit {completed_process.returncode}",
398
- is_sol_enabled,
454
+ True,
399
455
  )
400
456
  return parse_codex_jsonl_reply(
401
457
  completed_process.stdout,
402
458
  existing_session_id=session_id,
403
- is_sol_enabled=is_sol_enabled,
459
+ is_sol_enabled=True,
404
460
  )
405
461
 
406
462
 
@@ -417,6 +473,12 @@ def build_argument_parser() -> argparse.ArgumentParser:
417
473
  mode_group.add_argument("--bind", action="store_true")
418
474
  mode_group.add_argument("--resume", metavar=SOL_SESSION_ID_METAVAR)
419
475
  argument_parser.add_argument("--cwd", required=True, type=Path)
476
+ argument_parser.add_argument(
477
+ SOL_ENABLE_FLAG,
478
+ dest="is_sol_requested",
479
+ action="store_true",
480
+ help="Open the Sol rung for this invocation without an environment flag.",
481
+ )
420
482
  return argument_parser
421
483
 
422
484
 
@@ -430,12 +492,15 @@ def main(all_cli_arguments: Sequence[str]) -> int:
430
492
  Zero for a successful Sol response, or one for an explicit fallback.
431
493
  """
432
494
  parsed_arguments = build_argument_parser().parse_args(list(all_cli_arguments))
495
+ setting_by_name: Mapping[str, str] = os.environ
496
+ if parsed_arguments.is_sol_requested:
497
+ setting_by_name = {**os.environ, SOL_ENV_VAR: "1"}
433
498
  advisor_reply = run_codex_sol_advisor(
434
499
  prompt=sys.stdin.read(),
435
500
  working_directory=parsed_arguments.cwd,
436
501
  preflight=None,
437
502
  probe_path=None,
438
- setting_by_name=os.environ,
503
+ setting_by_name=setting_by_name,
439
504
  session_id=parsed_arguments.resume if not parsed_arguments.bind else None,
440
505
  process_runner=subprocess.run,
441
506
  )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  CODEX_EXECUTABLE: str = "codex"
6
+ ADVISOR_CODEX_EXECUTABLE_ENV_VAR: str = "ADVISOR_CODEX_EXECUTABLE"
6
7
  CODEX_READ_ONLY_SANDBOX: str = "read-only"
7
8
  CODEX_JSON_FLAG: str = "--json"
8
9
  CODEX_MODEL_FLAG: str = "--model"
@@ -26,3 +27,7 @@ SOL_CODEX_TIMEOUT_REASON: str = "Codex Sol xhigh request timed out"
26
27
  SOL_MALFORMED_JSONL_REASON: str = "Codex Sol xhigh returned malformed JSONL"
27
28
  SOL_MISSING_SESSION_REASON: str = "Codex Sol xhigh returned no session id"
28
29
  SOL_INVALID_SIGNAL_REASON: str = "Codex Sol xhigh returned an invalid guidance signal"
30
+ SOL_EXECUTABLE_NOT_FOUND_REASON: str = "Codex Sol xhigh could not find the codex executable on PATH"
31
+ SOL_FALLBACK_KIND_DECLINED: str = "declined"
32
+ SOL_FALLBACK_KIND_BROKEN: str = "broken"
33
+ SOL_ENABLE_FLAG: str = "--enable-sol"
@@ -37,6 +37,17 @@ USAGE_PROBE_PATH = (
37
37
  / "scripts"
38
38
  / "codex_usage_probe.py"
39
39
  )
40
+ WINDOWS_SHIM_PATH = r"C:\Users\me\AppData\Roaming\npm\codex.cmd"
41
+ ENABLED_SETTING_BY_NAME = {sol_advisor.SOL_ENV_VAR: "1"}
42
+
43
+
44
+ @pytest.fixture(autouse=True)
45
+ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
46
+ monkeypatch.setattr(
47
+ sol_advisor.shutil,
48
+ "which",
49
+ lambda name: "codex" if name == sol_advisor.CODEX_EXECUTABLE else None,
50
+ )
40
51
 
41
52
 
42
53
  def _probe_process(
@@ -130,6 +141,7 @@ def test_main_serializes_stable_result_field(monkeypatch: pytest.MonkeyPatch, ca
130
141
  sol_enabled=True,
131
142
  selected_tier=sol_advisor.ADVISOR_MODEL_TIER,
132
143
  outcome=sol_advisor.CODEX_BIND_SUCCESS_TOKEN,
144
+ fallback_kind=None,
133
145
  ),
134
146
  )
135
147
  monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
@@ -155,18 +167,165 @@ def test_bind_and_resume_arguments_match_installed_codex_interface() -> None:
155
167
  "read-only",
156
168
  "--json",
157
169
  ]
158
- assert sol_advisor.build_codex_arguments() == [
170
+ assert sol_advisor.build_codex_arguments("codex") == [
159
171
  *expected_common_arguments,
160
172
  "-",
161
173
  ]
162
- assert sol_advisor.build_codex_arguments("thread-1") == [
163
- *expected_common_arguments,
174
+ assert sol_advisor.build_codex_arguments(
175
+ WINDOWS_SHIM_PATH, session_id="thread-1"
176
+ ) == [
177
+ WINDOWS_SHIM_PATH,
178
+ *expected_common_arguments[1:],
164
179
  "resume",
165
180
  "thread-1",
166
181
  "-",
167
182
  ]
168
183
 
169
184
 
185
+ def test_resolve_codex_executable_prefers_the_env_var_override(
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ ) -> None:
188
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
189
+
190
+ resolved_executable = sol_advisor.resolve_codex_executable(
191
+ {sol_advisor.ADVISOR_CODEX_EXECUTABLE_ENV_VAR: WINDOWS_SHIM_PATH}
192
+ )
193
+
194
+ assert resolved_executable == WINDOWS_SHIM_PATH
195
+
196
+
197
+ def test_resolve_codex_executable_falls_back_to_which_search(
198
+ monkeypatch: pytest.MonkeyPatch,
199
+ ) -> None:
200
+ monkeypatch.setattr(
201
+ sol_advisor.shutil,
202
+ "which",
203
+ lambda name: (
204
+ WINDOWS_SHIM_PATH if name == sol_advisor.CODEX_EXECUTABLE else None
205
+ ),
206
+ )
207
+
208
+ resolved_executable = sol_advisor.resolve_codex_executable({})
209
+
210
+ assert resolved_executable == WINDOWS_SHIM_PATH
211
+
212
+
213
+ def test_resolve_codex_executable_returns_none_when_unresolved(
214
+ monkeypatch: pytest.MonkeyPatch,
215
+ ) -> None:
216
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
217
+
218
+ resolved_executable = sol_advisor.resolve_codex_executable({})
219
+
220
+ assert resolved_executable is None
221
+
222
+
223
+ def test_bind_falls_back_with_a_clear_reason_when_executable_is_missing(
224
+ monkeypatch: pytest.MonkeyPatch,
225
+ ) -> None:
226
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
227
+ calls: list[list[str]] = []
228
+
229
+ def process_runner(
230
+ arguments: list[str], **kwargs: object
231
+ ) -> subprocess.CompletedProcess[str]:
232
+ calls.append(arguments)
233
+ return _probe_process({"percent_left": 90})
234
+
235
+ reply = sol_advisor.run_codex_sol_advisor(
236
+ prompt="first consult",
237
+ working_directory=Path("."),
238
+ preflight=None,
239
+ probe_path=USAGE_PROBE_PATH,
240
+ session_id=None,
241
+ process_runner=process_runner,
242
+ setting_by_name={sol_advisor.SOL_ENV_VAR: "1"},
243
+ )
244
+
245
+ assert not reply.successful
246
+ assert reply.is_fallback
247
+ assert reply.reason is not None
248
+ assert "codex" in reply.reason
249
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
250
+ assert calls == []
251
+
252
+
253
+ def test_policy_fallbacks_are_marked_declined() -> None:
254
+ def gate_closed_runner(
255
+ arguments: list[str], **kwargs: object
256
+ ) -> subprocess.CompletedProcess[str]:
257
+ return _probe_process({"percent_left": 5})
258
+
259
+ disabled_reply = sol_advisor.run_codex_sol_advisor(
260
+ prompt="first consult",
261
+ working_directory=Path("."),
262
+ preflight=None,
263
+ probe_path=USAGE_PROBE_PATH,
264
+ session_id=None,
265
+ process_runner=gate_closed_runner,
266
+ setting_by_name={},
267
+ )
268
+ gate_closed_reply = sol_advisor.run_codex_sol_advisor(
269
+ prompt="first consult",
270
+ working_directory=Path("."),
271
+ preflight=None,
272
+ probe_path=USAGE_PROBE_PATH,
273
+ session_id=None,
274
+ process_runner=gate_closed_runner,
275
+ setting_by_name=ENABLED_SETTING_BY_NAME,
276
+ )
277
+
278
+ assert disabled_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
279
+ assert gate_closed_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
280
+
281
+
282
+ def test_probe_failure_fallback_is_marked_broken() -> None:
283
+ def failing_probe_runner(
284
+ arguments: list[str], **kwargs: object
285
+ ) -> subprocess.CompletedProcess[str]:
286
+ return _probe_process({}, returncode=3)
287
+
288
+ reply = sol_advisor.run_codex_sol_advisor(
289
+ prompt="first consult",
290
+ working_directory=Path("."),
291
+ preflight=None,
292
+ probe_path=USAGE_PROBE_PATH,
293
+ session_id=None,
294
+ process_runner=failing_probe_runner,
295
+ setting_by_name=ENABLED_SETTING_BY_NAME,
296
+ )
297
+
298
+ assert reply.is_fallback
299
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
300
+
301
+
302
+ def test_enable_sol_flag_opens_the_rung_without_an_environment_flag(
303
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
304
+ ) -> None:
305
+ captured_settings: dict[str, str] = {}
306
+
307
+ def fake_advisor(**kwargs: object) -> object:
308
+ captured_settings.update(dict(kwargs["setting_by_name"])) # type: ignore[arg-type]
309
+ return sol_advisor._reply_fallback(
310
+ "probe declined",
311
+ True,
312
+ fallback_kind=sol_advisor.SOL_FALLBACK_KIND_DECLINED,
313
+ )
314
+
315
+ monkeypatch.setattr(sol_advisor, "run_codex_sol_advisor", fake_advisor)
316
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
317
+ monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
318
+
319
+ exit_code = sol_advisor.main(
320
+ ["--bind", "--cwd", ".", sol_advisor.SOL_ENABLE_FLAG]
321
+ )
322
+ payload = json.loads(capsys.readouterr().out)
323
+
324
+ assert exit_code == 1
325
+ assert captured_settings[sol_advisor.SOL_ENV_VAR] == "1"
326
+ assert payload["fallback_kind"] == sol_advisor.SOL_FALLBACK_KIND_DECLINED
327
+
328
+
170
329
  def test_successful_probe_requires_finite_meter_above_configured_gate() -> None:
171
330
  def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
172
331
  return _probe_process({"percent_left": 90})
@@ -304,12 +463,12 @@ def test_bind_runs_probe_then_codex_with_read_only_xhigh_settings() -> None:
304
463
  probe_path=USAGE_PROBE_PATH,
305
464
  session_id=None,
306
465
  process_runner=process_runner,
307
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
466
+ setting_by_name=ENABLED_SETTING_BY_NAME,
308
467
  )
309
468
 
310
469
  assert reply.successful
311
470
  assert len(calls) == 2
312
- assert calls[1][0] == sol_advisor.build_codex_arguments()
471
+ assert calls[1][0] == sol_advisor.build_codex_arguments("codex")
313
472
  assert calls[1][1]["cwd"] == "."
314
473
  assert calls[1][1]["shell"] is False
315
474
  assert calls[1][1]["timeout"]
@@ -331,7 +490,7 @@ def test_team_advisor_path_preserves_sol_routing_fields() -> None:
331
490
  probe_path=USAGE_PROBE_PATH,
332
491
  session_id=None,
333
492
  process_runner=_two_step_process_runner(calls, guidance="PLAN\ninspect"),
334
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
493
+ setting_by_name=ENABLED_SETTING_BY_NAME,
335
494
  )
336
495
 
337
496
  assert reply.successful
@@ -358,7 +517,7 @@ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
358
517
  probe_path=USAGE_PROBE_PATH,
359
518
  session_id=None,
360
519
  process_runner=process_runner,
361
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
520
+ setting_by_name=ENABLED_SETTING_BY_NAME,
362
521
  )
363
522
 
364
523
  assert not reply.successful
@@ -369,7 +528,10 @@ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
369
528
  assert len(calls) == 1
370
529
 
371
530
 
372
- def test_disabled_flag_uses_default_optional_routing_inputs() -> None:
531
+ def test_disabled_flag_uses_default_optional_routing_inputs(
532
+ monkeypatch: pytest.MonkeyPatch,
533
+ ) -> None:
534
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
373
535
  reply = sol_advisor.run_codex_sol_advisor(
374
536
  prompt="first consult",
375
537
  working_directory=Path("."),
@@ -398,13 +560,13 @@ def test_resume_runs_the_usage_gate_before_codex() -> None:
398
560
  session_id="thread-1",
399
561
  probe_path=USAGE_PROBE_PATH,
400
562
  process_runner=_two_step_process_runner(calls, guidance="ENDORSE\nready"),
401
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
563
+ setting_by_name=ENABLED_SETTING_BY_NAME,
402
564
  )
403
565
 
404
566
  assert reply.successful
405
567
  assert calls == [
406
568
  [sys.executable, str(USAGE_PROBE_PATH)],
407
- sol_advisor.build_codex_arguments("thread-1"),
569
+ sol_advisor.build_codex_arguments("codex", session_id="thread-1"),
408
570
  ]
409
571
 
410
572
 
@@ -465,7 +627,7 @@ def test_codex_failure_modes_always_return_fallback(
465
627
  probe_path=USAGE_PROBE_PATH,
466
628
  session_id=None,
467
629
  process_runner=process_runner,
468
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
630
+ setting_by_name=ENABLED_SETTING_BY_NAME,
469
631
  )
470
632
 
471
633
  assert not reply.successful
@@ -15,6 +15,7 @@ Slash-command definitions installed into `~/.claude/commands/` by `bin/install.m
15
15
  | `pr-comments.md` | `/pr-comments` | Fetches and formats PR review comments for response |
16
16
  | `review-plan.md` | `/review-plan` | Reviews the current plan packet against code standards |
17
17
  | `right-size.md` | `/right-size` | Checks an implementation against the Right-Sized Engineering rules |
18
+ | `sr-loop.md` | `/sr-loop` | Runs the converging cleanup loop: /simplify passes until clean, then a code-review fix pass |
18
19
  | `sum.md` | `/sum` | Generates a formatted session summary for quick pickup in a new session |
19
20
 
20
21
  ## Format
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Converging cleanup loop - repeat /simplify until clean, then repeat /code-review --fix until clean
3
+ argument-hint: [PR URL, branch, or blank for the current diff]
4
+ ---
5
+
6
+ Run the converging cleanup loop on the target: `$ARGUMENTS` (blank means the
7
+ current branch's diff). Each phase invokes an existing skill with the Skill
8
+ tool and repeats it until a pass returns zero new findings.
9
+
10
+ ## Phase A — loop /simplify
11
+
12
+ 1. Invoke the `simplify` skill on the target (the same review the user gets
13
+ from `/simplify <target>`). Let it run its 4-lens fan-out and apply its
14
+ fixes.
15
+ 2. After each pass: run the scoped test suite (test files beside the touched
16
+ code, not the full repo suite), commit once (`git commit -F <file>`, body
17
+ written with the Write tool, Co-Authored-By line, 10-minute timeout — the
18
+ pre-commit gate runs its own tests), and push to the PR head branch. The PR
19
+ stays draft.
20
+ 3. Repeat the invocation. **Skips are sticky:** carry every adjudicated skip
21
+ forward into the next pass as "already adjudicated — do not re-report: ..."
22
+ context, or the loop never converges.
23
+ 4. Phase A converges when a full pass returns zero new findings. When the
24
+ previous pass changed only a line or two, a single combined-lens
25
+ confirmation agent may serve as the final pass.
26
+
27
+ ## Phase B — loop /code-review low --fix
28
+
29
+ 1. Invoke the `code-review` skill with arguments `low --fix` on the same
30
+ target. Let it report findings and apply its fixes.
31
+ 2. After any pass that changed files: test, commit, push as in Phase A.
32
+ 3. Repeat until a pass reports zero findings. Phase B usually converges in one
33
+ pass when Phase A ran first.
34
+
35
+ ## Finish
36
+
37
+ Report: passes run per phase, commits pushed with hashes, fixes applied, and
38
+ the standing skip list with reasons.
39
+
40
+ ## Constraints (observed under headless runs)
41
+
42
+ - Run this loop in the main session. A wrapper subagent cannot see the
43
+ `code-review` skill, and a nested agent's own subagent reports route to the
44
+ main session instead of back to it.
45
+ - Scope test runs to the touched files; a full-suite run can outlive one
46
+ foreground tool call.
47
+ - A hung or newly slow test suite is a finding to investigate, not an
48
+ inconvenience.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@ On exit 2 the script prints `{"error": ...}`. Ask the user for a manual reset ti
44
44
  In short: the resolver picks a bearer token, probes the OAuth usage endpoint the interactive `/usage` panel uses, and returns the session and weekly buckets with utilization and reset times. Token sources depend on the host:
45
45
 
46
46
  - **Desktop host** (the `CLAUDE_CODE_ENTRYPOINT` variable is `claude-desktop`): the resolver does not read the CLI credential file, which belongs to a different authentication session than the one the desktop app counts usage against. It uses the session ingress token when one is set, and otherwise takes the manual-override path.
47
- - **Every other host**: the resolver reads the Claude Code CLI's stored OAuth access token first (honored only while unexpired), then the session ingress bearer token file named by `CLAUDE_SESSION_INGRESS_TOKEN_FILE` (cloud sessions) when the credential token is unavailable.
47
+ - **Every other host**: the resolver reads the Claude Code CLI's stored OAuth access token first (honored only while unexpired), then the session ingress bearer token file named by `CLAUDE_SESSION_INGRESS_TOKEN_FILE` (cloud sessions) when the credential token is unavailable. The credential file lives under the directory `CLAUDE_CONFIG_DIR` names when that variable is set — a profile-isolated session's own account — else the home-directory default; the home file on a multi-profile machine can belong to a different account, whose meter is the wrong one to read.
48
48
 
49
49
  Fallbacks, in order: both token sources unavailable (expired/unreadable credential and no ingress file), a failed request, or a response with no readable session-window reset time all end in exit 2 — the manual-override ask above. The manual path works with no probe at all, so the skill functions even when both token sources are unavailable.
50
50
 
@@ -9,8 +9,10 @@
9
9
 
10
10
  With no ``--override``, the script resolves a bearer token. On the desktop
11
11
  host it uses only the session ingress token. On every other host it reads the
12
- Claude Code OAuth access token from the CLI credential file, then the session
13
- ingress token file when that credential is unavailable. It asks the OAuth
12
+ Claude Code OAuth access token from the CLI credential file under the
13
+ directory ``CLAUDE_CONFIG_DIR`` names when set, else the home-directory
14
+ default — then the session ingress token file when that credential is
15
+ unavailable. It asks the OAuth
14
16
  usage endpoint for the ``five_hour`` and ``seven_day`` windows. Exit code 2
15
17
  means the probe cannot resolve; the caller then asks the user for a manual
16
18
  reset time.
@@ -38,10 +40,12 @@ from usage_pause_constants.resolve_usage_window_constants import (
38
40
  BARE_MINUTES_PATTERN,
39
41
  CLOCK_HOUR_MAXIMUM,
40
42
  CLOCK_PATTERN,
43
+ CONFIG_DIR_ENV_VAR,
41
44
  CONTENT_TYPE_HEADER_NAME,
42
45
  CONTENT_TYPE_JSON,
43
46
  CREDENTIALS_ACCESS_TOKEN_KEY,
44
47
  CREDENTIALS_EXPIRES_AT_KEY,
48
+ CREDENTIALS_FILE_NAME,
45
49
  CREDENTIALS_OAUTH_SECTION_KEY,
46
50
  DESKTOP_ENTRYPOINT_VALUE,
47
51
  DURATION_PATTERN,
@@ -203,6 +207,27 @@ def plan_wakeup_stages(seconds_until_reset: int) -> list[int]:
203
207
  return stages
204
208
 
205
209
 
210
+ def default_credentials_path() -> Path:
211
+ """Locate the CLI credential file for the account this session runs as.
212
+
213
+ ::
214
+
215
+ CLAUDE_CONFIG_DIR=C:/profiles/mel -> C:/profiles/mel/.credentials.json
216
+ (variable unset or empty) -> ~/.claude/.credentials.json
217
+
218
+ A profile-isolated session keeps its credential under the config-dir
219
+ directory. On such a machine the home file belongs to a different
220
+ account. Reading the home file reports that other account's meter.
221
+
222
+ Returns:
223
+ The credential file for the session's own account.
224
+ """
225
+ config_directory = os.environ.get(CONFIG_DIR_ENV_VAR)
226
+ if config_directory:
227
+ return Path(config_directory) / CREDENTIALS_FILE_NAME
228
+ return Path.home().joinpath(*ALL_CREDENTIALS_RELATIVE_PATH_PARTS)
229
+
230
+
206
231
  def read_oauth_access_token(credentials_path: Path, now: datetime) -> str | None:
207
232
  """Read the CLI's OAuth access token when it is still valid.
208
233
 
@@ -523,7 +548,10 @@ def _parse_arguments() -> argparse.Namespace:
523
548
  parser.add_argument(
524
549
  "--credentials-path",
525
550
  default=None,
526
- help="Path to the CLI credential file; defaults to the home-directory location.",
551
+ help=(
552
+ "Path to the CLI credential file; defaults to the CLAUDE_CONFIG_DIR "
553
+ "location when that variable is set, else the home-directory location."
554
+ ),
527
555
  )
528
556
  return parser.parse_args()
529
557
 
@@ -558,7 +586,7 @@ def main() -> int:
558
586
  credentials_path = (
559
587
  Path(arguments.credentials_path)
560
588
  if arguments.credentials_path
561
- else Path.home().joinpath(*ALL_CREDENTIALS_RELATIVE_PATH_PARTS)
589
+ else default_credentials_path()
562
590
  )
563
591
  access_token = resolve_access_token(credentials_path, now)
564
592
  if access_token is None:
@@ -19,6 +19,7 @@ if str(SCRIPTS_DIRECTORY) not in sys.path:
19
19
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
20
20
 
21
21
  from usage_pause_constants.resolve_usage_window_constants import ( # noqa: E402
22
+ CONFIG_DIR_ENV_VAR,
22
23
  DESKTOP_ENTRYPOINT_VALUE,
23
24
  ENTRYPOINT_ENV_VAR,
24
25
  SESSION_INGRESS_TOKEN_FILE_ENV_VAR as INGRESS_TOKEN_FILE_ENV_VAR,
@@ -195,6 +196,31 @@ class TestReadOauthAccessToken:
195
196
  assert any("unreadable" in each_message for each_message in caplog.messages)
196
197
 
197
198
 
199
+ class TestDefaultCredentialsPath:
200
+ def should_use_the_config_dir_credential_file_when_the_variable_is_set(
201
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
202
+ ) -> None:
203
+ resolver = load_resolver_module()
204
+ monkeypatch.setenv(CONFIG_DIR_ENV_VAR, str(tmp_path))
205
+ assert resolver.default_credentials_path() == tmp_path / ".credentials.json"
206
+
207
+ def should_fall_back_to_the_home_credential_file_when_the_variable_is_unset(
208
+ self, monkeypatch: pytest.MonkeyPatch
209
+ ) -> None:
210
+ resolver = load_resolver_module()
211
+ monkeypatch.delenv(CONFIG_DIR_ENV_VAR, raising=False)
212
+ expected = Path.home() / ".claude" / ".credentials.json"
213
+ assert resolver.default_credentials_path() == expected
214
+
215
+ def should_fall_back_to_the_home_credential_file_when_the_variable_is_empty(
216
+ self, monkeypatch: pytest.MonkeyPatch
217
+ ) -> None:
218
+ resolver = load_resolver_module()
219
+ monkeypatch.setenv(CONFIG_DIR_ENV_VAR, "")
220
+ expected = Path.home() / ".claude" / ".credentials.json"
221
+ assert resolver.default_credentials_path() == expected
222
+
223
+
198
224
  class TestReadSessionIngressToken:
199
225
  def should_return_stripped_token_from_the_named_file(
200
226
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@@ -19,6 +19,8 @@ CONTENT_TYPE_JSON = "application/json"
19
19
  PROBE_TIMEOUT_SECONDS = 10
20
20
 
21
21
  ALL_CREDENTIALS_RELATIVE_PATH_PARTS = (".claude", ".credentials.json")
22
+ CONFIG_DIR_ENV_VAR = "CLAUDE_CONFIG_DIR"
23
+ CREDENTIALS_FILE_NAME = ".credentials.json"
22
24
  CREDENTIALS_OAUTH_SECTION_KEY = "claudeAiOauth"
23
25
  CREDENTIALS_ACCESS_TOKEN_KEY = "accessToken"
24
26
  CREDENTIALS_EXPIRES_AT_KEY = "expiresAt"
@@ -67,4 +69,4 @@ SOURCE_OVERRIDE = "override"
67
69
  EXIT_CODE_RESOLVED = 0
68
70
  EXIT_CODE_PROBE_UNAVAILABLE = 2
69
71
 
70
- LOGGING_FORMAT = "%(levelname)s %(name)s: %(message)s"
72
+ LOGGING_FORMAT = "%(levelname)s %(name)s: %(message)s"