claude-dev-env 1.90.0 → 1.92.1

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 (64) hide show
  1. package/CLAUDE.md +1 -52
  2. package/agents/clean-coder.md +2 -2
  3. package/agents/code-verifier.md +0 -1
  4. package/agents/test_agent_frontmatter.py +78 -0
  5. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  6. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  7. package/bin/install.mjs +1 -0
  8. package/docs/CODE_RULES.md +2 -2
  9. package/hooks/blocking/CLAUDE.md +5 -2
  10. package/hooks/blocking/code_rules_comments.py +2 -2
  11. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
  12. package/hooks/blocking/config/verified_commit_constants.py +2 -0
  13. package/hooks/blocking/conftest.py +115 -0
  14. package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
  15. package/hooks/blocking/pr_description_enforcer.py +46 -22
  16. package/hooks/blocking/pr_description_pr_number.py +5 -3
  17. package/hooks/blocking/pr_description_proof_of_work.py +367 -0
  18. package/hooks/blocking/precommit_code_rules_gate.py +5 -1
  19. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
  20. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
  21. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
  22. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
  23. package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
  24. package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
  25. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
  26. package/hooks/blocking/test_verification_verdict_store.py +11 -0
  27. package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
  28. package/hooks/blocking/test_verified_commit_gate.py +11 -0
  29. package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
  30. package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
  31. package/hooks/blocking/verification_verdict_store.py +73 -5
  32. package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
  33. package/hooks/blocking/verified_commit_gate.py +6 -0
  34. package/hooks/blocking/verifier_verdict_minter.py +6 -0
  35. package/hooks/hooks.json +7 -2
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  38. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
  39. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
  40. package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
  41. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
  42. package/hooks/validators/run_all_validators.py +216 -4
  43. package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
  44. package/package.json +1 -1
  45. package/rules/CLAUDE.md +7 -0
  46. package/rules/nas-ssh-invocation.md +21 -0
  47. package/rules/proof-of-work-pr-comments.md +26 -0
  48. package/rules/testing.md +0 -2
  49. package/scripts/CLAUDE.md +1 -0
  50. package/scripts/claude-chain.example.json +8 -0
  51. package/scripts/claude_chain_runner.py +400 -0
  52. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
  53. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
  54. package/scripts/sync_to_cursor/rules.py +1 -1
  55. package/scripts/test_claude_chain_runner.py +472 -0
  56. package/skills/CLAUDE.md +3 -0
  57. package/skills/orchestrator/SKILL.md +188 -0
  58. package/skills/orchestrator-refresh/SKILL.md +25 -0
  59. package/skills/usage-pause/SKILL.md +108 -0
  60. package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
  61. package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
  62. package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
  63. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
  64. package/system-prompts/software-engineer.xml +3 -2
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env python3
2
+ """Run a ``claude`` invocation through a config-driven fallback chain.
3
+
4
+ An automation that shells out to a single ``claude -p ...`` fails outright when
5
+ that account hits a usage limit. Other logged-in installs sit idle meanwhile.
6
+ This module runs the leading binary in the chain. It falls over to the next
7
+ binary only on a usage-limit failure. Every other outcome returns to the caller
8
+ unchanged.
9
+
10
+ The chain lives in ``~/.claude/claude-chain.json``. Copy the committed
11
+ ``claude-chain.example.json`` template there and list your binaries in fallback
12
+ order::
13
+
14
+ {"chain": [{"command": "claude", "extra_args": []},
15
+ {"command": "claude-ev", "extra_args": []}]}
16
+
17
+ A usage-limited primary falls over to the second binary::
18
+
19
+ primary claude -> exit 1, "usage limit reached" (falls over)
20
+ fallback claude-ev -> exit 0 (served)
21
+
22
+ Import ``run_claude`` for the outcome object, or run the module as a CLI::
23
+
24
+ python claude_chain_runner.py [--timeout-seconds N] -- <claude args...>
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import subprocess
32
+ import sys
33
+ from dataclasses import dataclass
34
+ from pathlib import Path
35
+
36
+ from dev_env_scripts_constants.claude_chain_constants import (
37
+ ALL_USAGE_LIMIT_SIGNATURES,
38
+ ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
39
+ ATTEMPT_STATUS_NONZERO_EXIT,
40
+ ATTEMPT_STATUS_SERVED,
41
+ ATTEMPT_STATUS_TIMEOUT,
42
+ ATTEMPT_STATUS_USAGE_LIMITED,
43
+ ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
44
+ ATTEMPT_SUMMARY_JOIN_SEPARATOR,
45
+ CHAIN_CONFIG_ERROR_EXIT_CODE,
46
+ CHAIN_EXHAUSTED_EXIT_CODE,
47
+ CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
48
+ CLAUDE_HOME_SUBDIRECTORY,
49
+ CLI_ARGUMENTS_SEPARATOR,
50
+ CLI_TIMEOUT_FLAG,
51
+ CONFIG_CHAIN_EMPTY_REASON,
52
+ CONFIG_CHAIN_KEY,
53
+ CONFIG_CHAIN_NOT_LIST_REASON,
54
+ CONFIG_COMMAND_KEY,
55
+ CONFIG_ENTRY_COMMAND_MISSING_REASON,
56
+ CONFIG_ENTRY_EXTRA_ARGS_INVALID_REASON,
57
+ CONFIG_ENTRY_NOT_OBJECT_REASON,
58
+ CONFIG_EXTRA_ARGS_KEY,
59
+ CONFIG_FILENAME,
60
+ CONFIG_INVALID_SHAPE_MESSAGE_TEMPLATE,
61
+ CONFIG_MALFORMED_MESSAGE_TEMPLATE,
62
+ CONFIG_MISSING_MESSAGE_TEMPLATE,
63
+ CONFIG_NOT_OBJECT_REASON,
64
+ CONFIG_UNREADABLE_MESSAGE_TEMPLATE,
65
+ DEFAULT_TIMEOUT_SECONDS,
66
+ EXAMPLE_CONFIG_FILENAME,
67
+ NO_COMPLETED_PROCESS_RETURN_CODE,
68
+ UTF8_ENCODING,
69
+ )
70
+
71
+
72
+ class ChainConfigurationError(Exception):
73
+ """Raised when the chain configuration is missing, unreadable, or malformed."""
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class ChainEntry:
78
+ """One binary in the fallback chain and its per-account extra arguments."""
79
+
80
+ command: str
81
+ extra_args: tuple[str, ...]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ChainAttempt:
86
+ """Record of one binary invocation and how it resolved."""
87
+
88
+ command: str
89
+ status: str
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ChainInvocationOutcome:
94
+ """Outcome of walking the chain for one call.
95
+
96
+ ``served_command`` names the binary whose response is returned. It is
97
+ ``None`` when no binary served the call: every entry was usage-limited or
98
+ missing, the invocation timed out, or the primary binary was absent. The
99
+ ``attempts`` trail records every binary tried and how it resolved.
100
+ """
101
+
102
+ served_command: str | None
103
+ returncode: int
104
+ stdout: str
105
+ stderr: str
106
+ attempts: tuple[ChainAttempt, ...]
107
+
108
+
109
+ chain_subprocess_runner = subprocess.run
110
+
111
+
112
+ def chain_config_path() -> Path:
113
+ """Return the path to the per-user chain configuration file."""
114
+ return Path.home() / CLAUDE_HOME_SUBDIRECTORY / CONFIG_FILENAME
115
+
116
+
117
+ def _invalid_shape_error(config_path: Path, reason: str) -> ChainConfigurationError:
118
+ return ChainConfigurationError(
119
+ CONFIG_INVALID_SHAPE_MESSAGE_TEMPLATE.format(
120
+ config_path=config_path,
121
+ reason=reason,
122
+ example_filename=EXAMPLE_CONFIG_FILENAME,
123
+ )
124
+ )
125
+
126
+
127
+ def _coerce_extra_args(raw_extra_args: object, config_path: Path) -> tuple[str, ...]:
128
+ if not isinstance(raw_extra_args, list) or not all(
129
+ isinstance(each_argument, str) for each_argument in raw_extra_args
130
+ ):
131
+ raise _invalid_shape_error(config_path, CONFIG_ENTRY_EXTRA_ARGS_INVALID_REASON)
132
+ return tuple(raw_extra_args)
133
+
134
+
135
+ def _parse_chain_entry(raw_entry: object, config_path: Path) -> ChainEntry:
136
+ if not isinstance(raw_entry, dict):
137
+ raise _invalid_shape_error(config_path, CONFIG_ENTRY_NOT_OBJECT_REASON)
138
+ command = raw_entry.get(CONFIG_COMMAND_KEY)
139
+ if not isinstance(command, str) or not command:
140
+ raise _invalid_shape_error(config_path, CONFIG_ENTRY_COMMAND_MISSING_REASON)
141
+ extra_args = _coerce_extra_args(
142
+ raw_entry.get(CONFIG_EXTRA_ARGS_KEY, []), config_path
143
+ )
144
+ return ChainEntry(command=command, extra_args=extra_args)
145
+
146
+
147
+ def _parse_chain_entries(parsed_config: object, config_path: Path) -> list[ChainEntry]:
148
+ if not isinstance(parsed_config, dict):
149
+ raise _invalid_shape_error(config_path, CONFIG_NOT_OBJECT_REASON)
150
+ raw_chain = parsed_config.get(CONFIG_CHAIN_KEY)
151
+ if not isinstance(raw_chain, list):
152
+ raise _invalid_shape_error(config_path, CONFIG_CHAIN_NOT_LIST_REASON)
153
+ if not raw_chain:
154
+ raise _invalid_shape_error(config_path, CONFIG_CHAIN_EMPTY_REASON)
155
+ return [
156
+ _parse_chain_entry(each_raw_entry, config_path) for each_raw_entry in raw_chain
157
+ ]
158
+
159
+
160
+ def load_chain(config_path: Path) -> list[ChainEntry]:
161
+ """Load the ordered fallback chain from *config_path*.
162
+
163
+ Args:
164
+ config_path: Path to the chain configuration JSON file.
165
+
166
+ Returns:
167
+ The ordered list of chain entries the file declares.
168
+
169
+ Raises:
170
+ ChainConfigurationError: When the file is absent, unreadable, not valid
171
+ JSON, or does not match the expected shape.
172
+ """
173
+ if not config_path.is_file():
174
+ raise ChainConfigurationError(
175
+ CONFIG_MISSING_MESSAGE_TEMPLATE.format(
176
+ config_path=config_path, example_filename=EXAMPLE_CONFIG_FILENAME
177
+ )
178
+ )
179
+ try:
180
+ raw_text = config_path.read_text(encoding=UTF8_ENCODING)
181
+ except OSError as read_error:
182
+ raise ChainConfigurationError(
183
+ CONFIG_UNREADABLE_MESSAGE_TEMPLATE.format(
184
+ config_path=config_path,
185
+ error=read_error,
186
+ example_filename=EXAMPLE_CONFIG_FILENAME,
187
+ )
188
+ ) from read_error
189
+ try:
190
+ parsed_config = json.loads(raw_text)
191
+ except json.JSONDecodeError as decode_error:
192
+ raise ChainConfigurationError(
193
+ CONFIG_MALFORMED_MESSAGE_TEMPLATE.format(
194
+ config_path=config_path,
195
+ error=decode_error,
196
+ example_filename=EXAMPLE_CONFIG_FILENAME,
197
+ )
198
+ ) from decode_error
199
+ return _parse_chain_entries(parsed_config, config_path)
200
+
201
+
202
+ def _build_invocation(entry: ChainEntry, all_claude_arguments: list[str]) -> list[str]:
203
+ return [entry.command, *all_claude_arguments, *entry.extra_args]
204
+
205
+
206
+ def _is_usage_limit_failure(completion: subprocess.CompletedProcess[str]) -> bool:
207
+ combined_text = f"{completion.stdout}{completion.stderr}".lower()
208
+ return any(
209
+ each_signature in combined_text for each_signature in ALL_USAGE_LIMIT_SIGNATURES
210
+ )
211
+
212
+
213
+ def _served_outcome(
214
+ served_command: str,
215
+ completion: subprocess.CompletedProcess[str],
216
+ all_attempts: list[ChainAttempt],
217
+ ) -> ChainInvocationOutcome:
218
+ return ChainInvocationOutcome(
219
+ served_command=served_command,
220
+ returncode=completion.returncode,
221
+ stdout=completion.stdout,
222
+ stderr=completion.stderr,
223
+ attempts=tuple(all_attempts),
224
+ )
225
+
226
+
227
+ def _timeout_streams(
228
+ timeout_error: subprocess.TimeoutExpired | None,
229
+ ) -> tuple[str, str]:
230
+ if timeout_error is None:
231
+ return "", ""
232
+ captured_stdout = (
233
+ timeout_error.stdout if isinstance(timeout_error.stdout, str) else ""
234
+ )
235
+ captured_stderr = (
236
+ timeout_error.stderr if isinstance(timeout_error.stderr, str) else ""
237
+ )
238
+ return captured_stdout, captured_stderr
239
+
240
+
241
+ def _no_process_outcome(
242
+ all_attempts: list[ChainAttempt],
243
+ timeout_error: subprocess.TimeoutExpired | None,
244
+ ) -> ChainInvocationOutcome:
245
+ captured_stdout, captured_stderr = _timeout_streams(timeout_error)
246
+ return ChainInvocationOutcome(
247
+ served_command=None,
248
+ returncode=NO_COMPLETED_PROCESS_RETURN_CODE,
249
+ stdout=captured_stdout,
250
+ stderr=captured_stderr,
251
+ attempts=tuple(all_attempts),
252
+ )
253
+
254
+
255
+ def _exhausted_outcome(
256
+ all_attempts: list[ChainAttempt],
257
+ last_usage_limited: subprocess.CompletedProcess[str] | None,
258
+ ) -> ChainInvocationOutcome:
259
+ if last_usage_limited is None:
260
+ return _no_process_outcome(all_attempts, None)
261
+ return ChainInvocationOutcome(
262
+ served_command=None,
263
+ returncode=last_usage_limited.returncode,
264
+ stdout=last_usage_limited.stdout,
265
+ stderr=last_usage_limited.stderr,
266
+ attempts=tuple(all_attempts),
267
+ )
268
+
269
+
270
+ def _classify_completion(
271
+ entry: ChainEntry,
272
+ completion: subprocess.CompletedProcess[str],
273
+ all_attempts: list[ChainAttempt],
274
+ ) -> ChainInvocationOutcome | None:
275
+ if completion.returncode == 0:
276
+ all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_SERVED))
277
+ return _served_outcome(entry.command, completion, all_attempts)
278
+ if _is_usage_limit_failure(completion):
279
+ all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_USAGE_LIMITED))
280
+ return None
281
+ all_attempts.append(ChainAttempt(entry.command, ATTEMPT_STATUS_NONZERO_EXIT))
282
+ return _served_outcome(entry.command, completion, all_attempts)
283
+
284
+
285
+ def run_claude(
286
+ all_claude_arguments: list[str], *, timeout_seconds: int
287
+ ) -> ChainInvocationOutcome:
288
+ """Run *all_claude_arguments* through the configured fallback chain.
289
+
290
+ The leading binary serves the call. Only a usage-limit failure (a non-zero
291
+ exit whose output carries a usage-limit signature) falls over to the next
292
+ binary. A missing fallback binary is skipped and the walk continues. A
293
+ timeout, a missing primary binary, or a non-zero exit without a usage-limit
294
+ signature stops the walk and returns that outcome unchanged.
295
+
296
+ Args:
297
+ all_claude_arguments: Arguments passed after the binary name, such as
298
+ ``["-p", prompt, "--strict-mcp-config"]``.
299
+ timeout_seconds: Timeout applied to each binary invocation.
300
+
301
+ Returns:
302
+ The outcome of the walk, naming the serving binary and the full
303
+ attempt trail.
304
+
305
+ Raises:
306
+ ChainConfigurationError: When the chain configuration cannot be loaded.
307
+ """
308
+ all_entries = load_chain(chain_config_path())
309
+ all_attempts: list[ChainAttempt] = []
310
+ last_usage_limited: subprocess.CompletedProcess[str] | None = None
311
+ for each_index, each_entry in enumerate(all_entries):
312
+ is_primary = each_index == 0
313
+ try:
314
+ completion = chain_subprocess_runner(
315
+ _build_invocation(each_entry, all_claude_arguments),
316
+ capture_output=True,
317
+ text=True,
318
+ timeout=timeout_seconds,
319
+ check=False,
320
+ )
321
+ except subprocess.TimeoutExpired as timeout_error:
322
+ all_attempts.append(
323
+ ChainAttempt(each_entry.command, ATTEMPT_STATUS_TIMEOUT)
324
+ )
325
+ return _no_process_outcome(all_attempts, timeout_error)
326
+ except FileNotFoundError:
327
+ all_attempts.append(
328
+ ChainAttempt(each_entry.command, ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND)
329
+ )
330
+ if is_primary:
331
+ return _no_process_outcome(all_attempts, None)
332
+ continue
333
+ terminal_outcome = _classify_completion(each_entry, completion, all_attempts)
334
+ if terminal_outcome is not None:
335
+ return terminal_outcome
336
+ last_usage_limited = completion
337
+ return _exhausted_outcome(all_attempts, last_usage_limited)
338
+
339
+
340
+ def _build_argument_parser() -> argparse.ArgumentParser:
341
+ parser = argparse.ArgumentParser(
342
+ description="Run a claude invocation through the fallback chain."
343
+ )
344
+ parser.add_argument(
345
+ CLI_TIMEOUT_FLAG,
346
+ dest="timeout_seconds",
347
+ type=int,
348
+ default=DEFAULT_TIMEOUT_SECONDS,
349
+ help="Timeout in seconds applied to each binary invocation.",
350
+ )
351
+ parser.add_argument("passthrough", nargs=argparse.REMAINDER)
352
+ return parser
353
+
354
+
355
+ def _strip_leading_separator(all_passthrough: list[str]) -> list[str]:
356
+ if all_passthrough and all_passthrough[0] == CLI_ARGUMENTS_SEPARATOR:
357
+ return all_passthrough[1:]
358
+ return all_passthrough
359
+
360
+
361
+ def _exhausted_message(all_attempts: tuple[ChainAttempt, ...]) -> str:
362
+ attempt_summary = ATTEMPT_SUMMARY_JOIN_SEPARATOR.join(
363
+ ATTEMPT_SUMMARY_ENTRY_TEMPLATE.format(
364
+ command=each_attempt.command, status=each_attempt.status
365
+ )
366
+ for each_attempt in all_attempts
367
+ )
368
+ return CHAIN_EXHAUSTED_MESSAGE_TEMPLATE.format(attempt_summary=attempt_summary)
369
+
370
+
371
+ def main(all_command_arguments: list[str]) -> int:
372
+ """Walk the chain for CLI arguments and return the process exit code.
373
+
374
+ Args:
375
+ all_command_arguments: The argument vector after the program name.
376
+
377
+ Returns:
378
+ The served binary's return code, a distinct code when the chain is
379
+ exhausted, or a distinct code when the configuration cannot be loaded.
380
+ """
381
+ parser = _build_argument_parser()
382
+ parsed_arguments = parser.parse_args(all_command_arguments)
383
+ all_claude_arguments = _strip_leading_separator(parsed_arguments.passthrough)
384
+ try:
385
+ chain_outcome = run_claude(
386
+ all_claude_arguments, timeout_seconds=parsed_arguments.timeout_seconds
387
+ )
388
+ except ChainConfigurationError as configuration_error:
389
+ print(str(configuration_error), file=sys.stderr)
390
+ return CHAIN_CONFIG_ERROR_EXIT_CODE
391
+ if chain_outcome.served_command is None:
392
+ print(_exhausted_message(chain_outcome.attempts), file=sys.stderr)
393
+ return CHAIN_EXHAUSTED_EXIT_CODE
394
+ sys.stdout.write(chain_outcome.stdout)
395
+ sys.stderr.write(chain_outcome.stderr)
396
+ return chain_outcome.returncode
397
+
398
+
399
+ if __name__ == "__main__":
400
+ sys.exit(main(sys.argv[1:]))
@@ -8,6 +8,7 @@ Named constants for scripts in `scripts/`. Follows the project convention that t
8
8
  |---|---|
9
9
  | `timing.py` | `sweep_empty_dirs.py` — `DEFAULT_AGE_SECONDS` (smallest age before an empty directory is eligible for removal) and `DEFAULT_POLL_INTERVAL` (seconds between sweep passes in continuous-watch mode) |
10
10
  | `gh_artifact_upload_constants.py` | `gh_artifact_upload.py` — the `artifacts` release tag, title, and notes body, the GitHub CLI binary name, the asset-name timestamp format and template, the asset download URL template, the notes-file suffix, and the text encoding |
11
+ | `claude_chain_constants.py` | `claude_chain_runner.py` — the chain config filename and home subdirectory, the usage-limit signature text, the per-binary status labels, the default timeout, CLI flag and separator tokens, config JSON keys, invalid-shape reason text, config-error and exhausted-chain message templates, and CLI exit codes |
11
12
  | `__init__.py` | Empty package marker |
12
13
 
13
14
  ## Convention
@@ -0,0 +1,124 @@
1
+ """Named constants for the claude fallback-chain runner.
2
+
3
+ Per the project's configuration conventions, every scalar and structural
4
+ constant the runner needs lives here rather than inline in the module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ UTF8_ENCODING: str = "utf-8"
10
+ """Encoding used to read the chain configuration file."""
11
+
12
+ CLAUDE_HOME_SUBDIRECTORY: str = ".claude"
13
+ """Per-user directory under the home directory that holds the chain config."""
14
+
15
+ CONFIG_FILENAME: str = "claude-chain.json"
16
+ """Real chain-configuration filename read from the user's home directory."""
17
+
18
+ EXAMPLE_CONFIG_FILENAME: str = "claude-chain.example.json"
19
+ """Committed template filename referenced in the config-error guidance."""
20
+
21
+ CONFIG_CHAIN_KEY: str = "chain"
22
+ """Top-level key whose value is the ordered list of chain entries."""
23
+
24
+ CONFIG_COMMAND_KEY: str = "command"
25
+ """Chain-entry key naming the binary to spawn."""
26
+
27
+ CONFIG_EXTRA_ARGS_KEY: str = "extra_args"
28
+ """Chain-entry key holding per-account arguments appended to each invocation."""
29
+
30
+ ALL_USAGE_LIMIT_SIGNATURES: tuple[str, ...] = (
31
+ "hit your session limit",
32
+ "usage limit reached",
33
+ "out of usage",
34
+ "usage quota exceeded",
35
+ )
36
+ """Case-insensitive substrings that mark a non-zero exit as a usage-limit refusal."""
37
+
38
+ ATTEMPT_STATUS_SERVED: str = "served"
39
+ """Status recorded when a binary exits zero and serves the call."""
40
+
41
+ ATTEMPT_STATUS_USAGE_LIMITED: str = "usage_limited"
42
+ """Status recorded when a binary fails with a usage-limit signature."""
43
+
44
+ ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND: str = "executable_not_found"
45
+ """Status recorded when a binary is not installed."""
46
+
47
+ ATTEMPT_STATUS_NONZERO_EXIT: str = "nonzero_exit"
48
+ """Status recorded when a binary fails without a usage-limit signature."""
49
+
50
+ ATTEMPT_STATUS_TIMEOUT: str = "timeout"
51
+ """Status recorded when a binary exceeds the invocation timeout."""
52
+
53
+ DEFAULT_TIMEOUT_SECONDS: int = 300
54
+ """Timeout applied to each binary invocation when the caller names none."""
55
+
56
+ NO_COMPLETED_PROCESS_RETURN_CODE: int = 1
57
+ """Return code carried on the result when no binary produced a completed process."""
58
+
59
+ CHAIN_EXHAUSTED_EXIT_CODE: int = 2
60
+ """CLI exit code when no binary in the chain served the call."""
61
+
62
+ CHAIN_CONFIG_ERROR_EXIT_CODE: int = 3
63
+ """CLI exit code when the chain configuration is missing or invalid."""
64
+
65
+ CLI_TIMEOUT_FLAG: str = "--timeout-seconds"
66
+ """CLI flag that overrides the per-invocation timeout in seconds."""
67
+
68
+ CLI_ARGUMENTS_SEPARATOR: str = "--"
69
+ """CLI token separating runner flags from the passthrough claude arguments."""
70
+
71
+ CONFIG_NOT_OBJECT_REASON: str = "the top-level value is not a JSON object"
72
+ """Reason detail when the config root is not an object."""
73
+
74
+ CONFIG_CHAIN_NOT_LIST_REASON: str = "the 'chain' key is missing or not a list"
75
+ """Reason detail when the chain key is absent or the wrong type."""
76
+
77
+ CONFIG_CHAIN_EMPTY_REASON: str = "the 'chain' list is empty"
78
+ """Reason detail when the chain contains no entries."""
79
+
80
+ CONFIG_ENTRY_NOT_OBJECT_REASON: str = "a chain entry is not a JSON object"
81
+ """Reason detail when a chain entry is not an object."""
82
+
83
+ CONFIG_ENTRY_COMMAND_MISSING_REASON: str = "a chain entry has no string 'command'"
84
+ """Reason detail when a chain entry lacks a usable command."""
85
+
86
+ CONFIG_ENTRY_EXTRA_ARGS_INVALID_REASON: str = (
87
+ "a chain entry's 'extra_args' is not a list of strings"
88
+ )
89
+ """Reason detail when a chain entry's extra_args value is the wrong shape."""
90
+
91
+ CONFIG_MISSING_MESSAGE_TEMPLATE: str = (
92
+ "Claude chain config not found at {config_path}. Copy {example_filename} to "
93
+ "{config_path} and list your logged-in claude binaries in fallback order."
94
+ )
95
+ """Guidance shown when the config file is absent."""
96
+
97
+ CONFIG_UNREADABLE_MESSAGE_TEMPLATE: str = (
98
+ "Cannot read claude chain config at {config_path}: {error}. "
99
+ "See {example_filename} for the expected shape."
100
+ )
101
+ """Guidance shown when the config file cannot be read."""
102
+
103
+ CONFIG_MALFORMED_MESSAGE_TEMPLATE: str = (
104
+ "Malformed JSON in claude chain config at {config_path}: {error}. "
105
+ "See {example_filename} for the expected shape."
106
+ )
107
+ """Guidance shown when the config file is not valid JSON."""
108
+
109
+ CONFIG_INVALID_SHAPE_MESSAGE_TEMPLATE: str = (
110
+ "Invalid claude chain config at {config_path}: {reason}. "
111
+ "See {example_filename} for the expected shape."
112
+ )
113
+ """Guidance shown when the config JSON does not match the expected shape."""
114
+
115
+ CHAIN_EXHAUSTED_MESSAGE_TEMPLATE: str = (
116
+ "No claude binary in the chain served the call. Attempts: {attempt_summary}"
117
+ )
118
+ """CLI stderr message when the walk ends without a serving binary."""
119
+
120
+ ATTEMPT_SUMMARY_ENTRY_TEMPLATE: str = "{command}={status}"
121
+ """Per-attempt fragment used to build the exhausted-chain summary."""
122
+
123
+ ATTEMPT_SUMMARY_JOIN_SEPARATOR: str = ", "
124
+ """Separator joining per-attempt fragments in the exhausted-chain summary."""
@@ -68,7 +68,7 @@ _merged_mapping_key_order = (
68
68
  )
69
69
 
70
70
  _code_standards_section_order = (
71
- "COMMENT PRESERVATION (ABSOLUTE RULE)",
71
+ "COMMENT PRESERVATION",
72
72
  "CORE PRINCIPLES",
73
73
  "⚡ HOOK-ENFORCED RULES",
74
74
  "3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",