claude-dev-env 1.89.0 → 1.92.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.
Files changed (77) hide show
  1. package/CLAUDE.md +0 -13
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
  5. package/agents/clean-coder.md +2 -2
  6. package/agents/code-verifier.md +0 -1
  7. package/agents/test_agent_frontmatter.py +78 -0
  8. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  9. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  10. package/bin/install.mjs +1 -0
  11. package/docs/CODE_RULES.md +2 -2
  12. package/hooks/blocking/CLAUDE.md +6 -2
  13. package/hooks/blocking/code_rules_comments.py +2 -2
  14. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
  15. package/hooks/blocking/config/verified_commit_constants.py +2 -0
  16. package/hooks/blocking/conftest.py +115 -0
  17. package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
  18. package/hooks/blocking/pr_description_enforcer.py +46 -22
  19. package/hooks/blocking/pr_description_pr_number.py +5 -3
  20. package/hooks/blocking/pr_description_proof_of_work.py +367 -0
  21. package/hooks/blocking/precommit_code_rules_gate.py +5 -1
  22. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
  23. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
  24. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
  25. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
  26. package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
  27. package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +11 -0
  30. package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
  31. package/hooks/blocking/test_verified_commit_gate.py +11 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
  33. package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
  34. package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
  35. package/hooks/blocking/verification_verdict_store.py +73 -5
  36. package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
  37. package/hooks/blocking/verified_commit_gate.py +6 -0
  38. package/hooks/blocking/verifier_verdict_minter.py +6 -0
  39. package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
  40. package/hooks/hooks.json +32 -2
  41. package/hooks/hooks_constants/CLAUDE.md +4 -0
  42. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  43. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
  44. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
  45. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
  46. package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
  47. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
  48. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
  49. package/hooks/lifecycle/CLAUDE.md +3 -1
  50. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
  51. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
  52. package/hooks/validators/run_all_validators.py +216 -4
  53. package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
  54. package/package.json +1 -1
  55. package/rules/CLAUDE.md +3 -0
  56. package/rules/durable-post-artifacts.md +35 -0
  57. package/rules/nas-ssh-invocation.md +21 -0
  58. package/rules/proof-of-work-pr-comments.md +26 -0
  59. package/scripts/CLAUDE.md +2 -0
  60. package/scripts/claude-chain.example.json +8 -0
  61. package/scripts/claude_chain_runner.py +400 -0
  62. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  63. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
  64. package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
  65. package/scripts/gh_artifact_upload.py +256 -0
  66. package/scripts/sync_to_cursor/rules.py +1 -1
  67. package/scripts/test_claude_chain_runner.py +472 -0
  68. package/scripts/tests/test_gh_artifact_upload.py +205 -0
  69. package/skills/CLAUDE.md +3 -0
  70. package/skills/team-advisor/SKILL.md +188 -0
  71. package/skills/team-advisor-refresh/SKILL.md +25 -0
  72. package/skills/usage-pause/SKILL.md +108 -0
  73. package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
  74. package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
  75. package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
  76. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
  77. package/system-prompts/software-engineer.xml +3 -2
@@ -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."""
@@ -0,0 +1,43 @@
1
+ """Constants for the gh_artifact_upload script.
2
+
3
+ Per the project's configuration conventions, script-level scalar constants
4
+ live in dev_env_scripts_constants alongside timing.py.
5
+ """
6
+
7
+ ARTIFACTS_RELEASE_TAG: str = "artifacts"
8
+ """Release tag that holds durable post artifacts."""
9
+
10
+ ARTIFACTS_RELEASE_TITLE: str = "Durable post artifacts"
11
+ """Human-readable title for the artifacts release."""
12
+
13
+ ARTIFACTS_RELEASE_NOTES: str = (
14
+ "Permanent storage for binary artifacts linked from GitHub issue and pull "
15
+ "request posts. Job scratch directories are ephemeral and get cleaned soon "
16
+ "after a run; assets uploaded to this prerelease persist so a durable post "
17
+ "can link a stable URL."
18
+ )
19
+ """Release body written when the artifacts release is first created."""
20
+
21
+ GH_BINARY_NAME: str = "gh"
22
+ """The GitHub CLI executable name."""
23
+
24
+ ASSET_NAME_TIMESTAMP_FORMAT: str = "%Y%m%d_%H%M%S"
25
+ """strftime format for the timestamp prefix on an uploaded asset name."""
26
+
27
+ ASSET_NAME_TEMPLATE: str = "{timestamp}_{basename}"
28
+ """Template joining the timestamp prefix and the source file basename."""
29
+
30
+ RELEASE_ASSETS_JSON_KEY: str = "assets"
31
+ """``gh release view --json`` field holding the release's asset list."""
32
+
33
+ ASSET_URL_JSON_KEY: str = "url"
34
+ """Asset field carrying the browser download URL GitHub serves."""
35
+
36
+ ASSET_CREATED_AT_JSON_KEY: str = "createdAt"
37
+ """Asset field carrying the ISO 8601 creation timestamp."""
38
+
39
+ NOTES_FILE_SUFFIX: str = ".md"
40
+ """Suffix for the temporary release-notes file."""
41
+
42
+ UTF8_ENCODING: str = "utf-8"
43
+ """Text encoding for subprocess output and temp files."""
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ """Upload a file to a repo's durable 'artifacts' release and print its URL.
3
+
4
+ A durable GitHub post (issue, PR, comment, review) must not link a file under a
5
+ job scratch directory or worktree, because that scratch is cleaned soon after
6
+ the run while the post lives forever. Binary artifacts belong in a permanent
7
+ place instead. This tool ensures the repo has a prerelease tagged ``artifacts``,
8
+ uploads the given file under a timestamped asset name, and prints the permanent
9
+ download URL a post can safely link. The timestamped name keeps each upload a
10
+ distinct asset. An upload never overwrites an earlier one. A same-name collision
11
+ fails loudly instead of replacing the bytes an existing URL already serves.
12
+
13
+ Usage::
14
+
15
+ python3 gh_artifact_upload.py <file-path> <owner/repo>
16
+ -> https://github.com/<owner>/<repo>/releases/download/artifacts/20260707_140233_contact_sheet.png
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import datetime
23
+ import json
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ import tempfile
28
+ from pathlib import Path
29
+
30
+ from dev_env_scripts_constants.gh_artifact_upload_constants import (
31
+ ARTIFACTS_RELEASE_NOTES,
32
+ ARTIFACTS_RELEASE_TAG,
33
+ ARTIFACTS_RELEASE_TITLE,
34
+ ASSET_CREATED_AT_JSON_KEY,
35
+ ASSET_NAME_TEMPLATE,
36
+ ASSET_NAME_TIMESTAMP_FORMAT,
37
+ ASSET_URL_JSON_KEY,
38
+ GH_BINARY_NAME,
39
+ NOTES_FILE_SUFFIX,
40
+ RELEASE_ASSETS_JSON_KEY,
41
+ UTF8_ENCODING,
42
+ )
43
+
44
+
45
+ class ArtifactUploadError(Exception):
46
+ """Raised when creating the release or uploading the asset fails."""
47
+
48
+
49
+ def _run_gh(all_arguments: list[str]) -> subprocess.CompletedProcess[str]:
50
+ return subprocess.run(
51
+ [GH_BINARY_NAME, *all_arguments],
52
+ capture_output=True,
53
+ text=True,
54
+ encoding=UTF8_ENCODING,
55
+ check=False,
56
+ )
57
+
58
+
59
+ def artifacts_release_exists(repository: str) -> bool:
60
+ """Return whether the durable artifacts release already exists in the repo.
61
+
62
+ Args:
63
+ repository: The ``owner/repo`` slug.
64
+
65
+ Returns:
66
+ True when ``gh release view`` finds the ``artifacts`` release.
67
+ """
68
+ completion = _run_gh(
69
+ [
70
+ "release",
71
+ "view",
72
+ ARTIFACTS_RELEASE_TAG,
73
+ "--repo",
74
+ repository,
75
+ "--json",
76
+ "tagName",
77
+ ]
78
+ )
79
+ return completion.returncode == 0
80
+
81
+
82
+ def ensure_artifacts_release(repository: str) -> None:
83
+ """Create the artifacts prerelease when the repo does not already have it.
84
+
85
+ Args:
86
+ repository: The ``owner/repo`` slug.
87
+
88
+ Raises:
89
+ ArtifactUploadError: When the release cannot be created.
90
+ """
91
+ if artifacts_release_exists(repository):
92
+ return
93
+ with tempfile.NamedTemporaryFile(
94
+ mode="w", suffix=NOTES_FILE_SUFFIX, delete=False, encoding=UTF8_ENCODING
95
+ ) as notes_file:
96
+ notes_file.write(ARTIFACTS_RELEASE_NOTES)
97
+ notes_path = notes_file.name
98
+ completion = _run_gh(
99
+ [
100
+ "release",
101
+ "create",
102
+ ARTIFACTS_RELEASE_TAG,
103
+ "--repo",
104
+ repository,
105
+ "--prerelease",
106
+ "--title",
107
+ ARTIFACTS_RELEASE_TITLE,
108
+ "--notes-file",
109
+ notes_path,
110
+ ]
111
+ )
112
+ Path(notes_path).unlink(missing_ok=True)
113
+ if completion.returncode != 0:
114
+ raise ArtifactUploadError(completion.stderr.strip())
115
+
116
+
117
+ def timestamped_asset_name(file_path: str) -> str:
118
+ """Return ``YYYYMMDD_HHMMSS_<basename>`` for the given file path.
119
+
120
+ Args:
121
+ file_path: The source file to be uploaded.
122
+
123
+ Returns:
124
+ The asset name that prefixes the basename with the current timestamp.
125
+ """
126
+ current_timestamp = datetime.datetime.now().strftime(ASSET_NAME_TIMESTAMP_FORMAT)
127
+ return ASSET_NAME_TEMPLATE.format(
128
+ timestamp=current_timestamp, basename=Path(file_path).name
129
+ )
130
+
131
+
132
+ def _uploaded_asset_download_url(repository: str) -> str:
133
+ """Read the just-uploaded asset's real download URL back from GitHub.
134
+
135
+ ::
136
+
137
+ upload "My Report.png" -> GitHub stores "..._My.Report.png"
138
+ read-back -> the URL GitHub serves for that sanitized name
139
+
140
+ GitHub sanitizes an asset filename on upload: spaces and other characters
141
+ become periods. The stored name, and so the download URL, can differ from
142
+ the local name. Reading the release back returns the URL GitHub actually
143
+ serves for the most recently created asset, which is the one just uploaded.
144
+
145
+ Args:
146
+ repository: The ``owner/repo`` slug.
147
+
148
+ Returns:
149
+ The browser download URL of the newest artifacts-release asset.
150
+
151
+ Raises:
152
+ ArtifactUploadError: When the read-back fails, the response cannot be
153
+ parsed, or no asset carries a download URL.
154
+ """
155
+ completion = _run_gh(
156
+ [
157
+ "release",
158
+ "view",
159
+ ARTIFACTS_RELEASE_TAG,
160
+ "--repo",
161
+ repository,
162
+ "--json",
163
+ RELEASE_ASSETS_JSON_KEY,
164
+ ]
165
+ )
166
+ if completion.returncode != 0:
167
+ raise ArtifactUploadError(completion.stderr.strip())
168
+ try:
169
+ parsed_release = json.loads(completion.stdout)
170
+ except json.JSONDecodeError as decode_error:
171
+ raise ArtifactUploadError(f"could not read release assets: {decode_error}")
172
+ all_assets = (
173
+ parsed_release.get(RELEASE_ASSETS_JSON_KEY, [])
174
+ if isinstance(parsed_release, dict)
175
+ else []
176
+ )
177
+ all_dated_assets = [
178
+ each_asset
179
+ for each_asset in all_assets
180
+ if isinstance(each_asset, dict) and each_asset.get(ASSET_CREATED_AT_JSON_KEY)
181
+ ]
182
+ if not all_dated_assets:
183
+ raise ArtifactUploadError("uploaded asset not found on read-back")
184
+ newest_asset = max(
185
+ all_dated_assets,
186
+ key=lambda each_asset: each_asset[ASSET_CREATED_AT_JSON_KEY],
187
+ )
188
+ asset_url = newest_asset.get(ASSET_URL_JSON_KEY)
189
+ if not isinstance(asset_url, str) or not asset_url:
190
+ raise ArtifactUploadError("uploaded asset carries no download URL on read-back")
191
+ return asset_url
192
+
193
+
194
+ def upload_artifact(file_path: str, repository: str) -> str:
195
+ """Upload a file to the artifacts release and return its permanent URL.
196
+
197
+ Args:
198
+ file_path: The source file to upload.
199
+ repository: The ``owner/repo`` slug.
200
+
201
+ Returns:
202
+ The permanent download URL for the uploaded asset.
203
+
204
+ Raises:
205
+ ArtifactUploadError: When the file is missing or the upload fails.
206
+ """
207
+ source_path = Path(file_path)
208
+ if not source_path.is_file():
209
+ raise ArtifactUploadError(f"file not found: {file_path}")
210
+ ensure_artifacts_release(repository)
211
+ asset_name = timestamped_asset_name(file_path)
212
+ with tempfile.TemporaryDirectory() as staging_directory:
213
+ staged_asset_path = Path(staging_directory) / asset_name
214
+ shutil.copyfile(source_path, staged_asset_path)
215
+ completion = _run_gh(
216
+ [
217
+ "release",
218
+ "upload",
219
+ ARTIFACTS_RELEASE_TAG,
220
+ str(staged_asset_path),
221
+ "--repo",
222
+ repository,
223
+ ]
224
+ )
225
+ if completion.returncode != 0:
226
+ raise ArtifactUploadError(completion.stderr.strip())
227
+ return _uploaded_asset_download_url(repository)
228
+
229
+
230
+ def main() -> int:
231
+ """Parse arguments, upload the file, and print the permanent asset URL.
232
+
233
+ Returns:
234
+ ``0`` when the upload succeeds, ``1`` when it fails.
235
+ """
236
+ parser = argparse.ArgumentParser(
237
+ description="Upload a file to a repo's durable 'artifacts' release."
238
+ )
239
+ parser.add_argument("file_path", help="Path to the file to upload.")
240
+ parser.add_argument(
241
+ "repository", metavar="owner/repo", help="Target GitHub repository slug."
242
+ )
243
+ parsed_arguments = parser.parse_args()
244
+ try:
245
+ asset_url = upload_artifact(
246
+ parsed_arguments.file_path, parsed_arguments.repository
247
+ )
248
+ except ArtifactUploadError as upload_error:
249
+ print(f"gh-artifact-upload failed: {upload_error}", file=sys.stderr)
250
+ return 1
251
+ print(asset_url)
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ sys.exit(main())
@@ -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",