claude-dev-env 2.5.0 → 2.7.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 (153) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/_shared/pr-loop/worker-spawn.md +3 -1
  14. package/agents/CLAUDE.md +1 -1
  15. package/agents/code-verifier.md +36 -7
  16. package/bin/codex-compat.mjs +104 -0
  17. package/bin/codex-compat.test.mjs +51 -0
  18. package/codex-capability-map.json +13 -0
  19. package/docs/CODE_RULES.md +2 -0
  20. package/docs/codex-compatibility.md +25 -0
  21. package/docs/nas-ssh-invocation.md +96 -12
  22. package/docs/references/code-review-enforcement.md +31 -6
  23. package/hooks/blocking/CLAUDE.md +3 -0
  24. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  25. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  26. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  27. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  28. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  29. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  30. package/hooks/blocking/state_description_blocker.py +1 -1
  31. package/hooks/blocking/stop_dispatcher.py +1 -1
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  33. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  34. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  35. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  36. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  37. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  38. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  39. package/hooks/git-hooks/CLAUDE.md +3 -0
  40. package/hooks/git-hooks/conftest.py +30 -0
  41. package/hooks/git-hooks/gate_utils.py +2 -2
  42. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  43. package/hooks/git-hooks/pre_push.py +75 -4
  44. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  45. package/hooks/git-hooks/test_config.py +0 -15
  46. package/hooks/git-hooks/test_gate_utils.py +3 -15
  47. package/hooks/git-hooks/test_pre_commit.py +1 -15
  48. package/hooks/git-hooks/test_pre_push.py +236 -27
  49. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  50. package/hooks/hooks.json +0 -12
  51. package/hooks/hooks_constants/CLAUDE.md +5 -1
  52. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  53. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  54. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  55. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  56. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  57. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  58. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  59. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  60. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  61. package/package.json +4 -2
  62. package/rules/CLAUDE.md +17 -23
  63. package/rules/agent-spawn-protocol.md +6 -6
  64. package/rules/anti-corollary-tests.md +1 -1
  65. package/rules/bdd.md +1 -1
  66. package/rules/cleanup-temp-files.md +10 -4
  67. package/rules/code-standards.md +7 -0
  68. package/rules/conservative-action.md +1 -5
  69. package/rules/context7.md +0 -4
  70. package/rules/destructive-commands.md +47 -0
  71. package/rules/doc-inventory-integrity.md +48 -0
  72. package/rules/doc-prose-cuts.md +58 -0
  73. package/rules/docstring-prose-matches-implementation.md +10 -2
  74. package/rules/durable-post-artifacts.md +0 -4
  75. package/rules/eli11-replies.md +31 -0
  76. package/rules/explore-thoroughly.md +4 -4
  77. package/rules/falsify-before-green.md +68 -0
  78. package/rules/file-global-constants.md +1 -1
  79. package/rules/filesystem-search.md +51 -0
  80. package/rules/gh-cli-conventions.md +27 -0
  81. package/rules/git-workflow.md +26 -0
  82. package/rules/hedging-claims.md +9 -0
  83. package/rules/long-horizon-autonomy.md +0 -4
  84. package/rules/measurement-denominators.md +48 -0
  85. package/rules/nas-ssh-invocation.md +23 -5
  86. package/rules/parallel-tools.md +2 -2
  87. package/rules/plain-illustrative-docstrings.md +3 -7
  88. package/rules/plain-language.md +2 -0
  89. package/rules/proof-of-work-pr-comments.md +0 -4
  90. package/rules/re-stage-before-commit.md +2 -0
  91. package/rules/research-mode.md +10 -0
  92. package/rules/shell-invocation.md +21 -0
  93. package/rules/testing.md +4 -0
  94. package/rules/verified-commit-gate-skip.md +3 -27
  95. package/rules/verify-before-asking.md +5 -0
  96. package/rules/windows-filesystem-safe.md +1 -1
  97. package/rules/workers-done-before-complete.md +4 -0
  98. package/scripts/CLAUDE.md +3 -3
  99. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  100. package/scripts/codex_capability_bridge.py +171 -0
  101. package/scripts/codex_compat_materializer.py +1087 -0
  102. package/scripts/codex_compat_watcher.py +502 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  104. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  105. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
  106. package/scripts/grok_headless_runner.py +213 -16
  107. package/scripts/invoke_code_review.py +11 -4
  108. package/scripts/resolve_worker_spawn.py +56 -10
  109. package/scripts/spawn_grok_batch.py +43 -22
  110. package/scripts/sync_to_cursor/rules.py +0 -10
  111. package/scripts/test_grok_headless_runner.py +592 -10
  112. package/scripts/test_invoke_code_review.py +143 -0
  113. package/scripts/test_invoke_code_review_chain.py +1 -1
  114. package/scripts/test_invoke_code_review_contract.py +1 -1
  115. package/scripts/test_resolve_worker_spawn.py +179 -15
  116. package/scripts/test_spawn_grok_batch.py +225 -22
  117. package/scripts/tests/test_code_review_constants.py +80 -0
  118. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  119. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  120. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  121. package/scripts/tests/test_sync_to_cursor.py +0 -1
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
  123. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
  124. package/skills/autoconverge/workflow/converge.mjs +64 -15
  125. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  126. package/skills/condensing-instructions/SKILL.md +42 -51
  127. package/skills/fresh-branch/CLAUDE.md +1 -1
  128. package/skills/fresh-branch/SKILL.md +5 -6
  129. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  131. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  132. package/skills/grok-spawn/SKILL.md +5 -3
  133. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  134. package/skills/orchestrator/SKILL.md +23 -9
  135. package/skills/orchestrator-refresh/SKILL.md +20 -1
  136. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  137. package/skills/session-log/SKILL.md +1 -1
  138. package/rules/claude-md-orphan-file.md +0 -28
  139. package/rules/cleanup-command-forms.md +0 -23
  140. package/rules/code-reviews.md +0 -11
  141. package/rules/env-var-table-code-drift.md +0 -10
  142. package/rules/gh-body-file.md +0 -5
  143. package/rules/gh-paginate.md +0 -3
  144. package/rules/hook-prose-matches-detector.md +0 -15
  145. package/rules/no-historical-clutter.md +0 -26
  146. package/rules/no-inline-destructive-literals.md +0 -9
  147. package/rules/no-justification-noise.md +0 -61
  148. package/rules/package-inventory-stale-entry.md +0 -25
  149. package/rules/right-sized-engineering.md +0 -28
  150. package/rules/self-contained-docs.md +0 -17
  151. package/rules/shell-invocation-policy.md +0 -5
  152. package/rules/state-what-is.md +0 -25
  153. package/rules/tdd.md +0 -7
@@ -1,53 +1,43 @@
1
1
  """Behavioral tests for the staged_test_running parts module."""
2
2
 
3
- import subprocess
4
3
  from pathlib import Path
5
4
 
6
- from code_rules_gate_parts import git_file_sets, staged_test_running
5
+ import pytest
7
6
 
8
-
9
- def _git(repository_root: Path, *arguments: str) -> None:
10
- subprocess.run(
11
- ["git", *arguments],
12
- cwd=str(repository_root),
13
- check=True,
14
- capture_output=True,
15
- env=git_file_sets.repository_environment(),
16
- )
7
+ from code_rules_gate_parts import staged_test_running
8
+ from code_rules_gate_parts.tests._repo_test_helpers import repository_root_without_pytest_config
17
9
 
18
10
 
19
- def _init_repository(repository_root: Path) -> None:
20
- _git(repository_root, "init", "--initial-branch=main")
21
- _git(repository_root, "config", "user.email", "test@example.com")
22
- _git(repository_root, "config", "user.name", "Test")
23
- _git(repository_root, "config", "commit.gpgsign", "false")
24
- (repository_root / "seed.txt").write_text("seed\n", encoding="utf-8")
25
- _git(repository_root, "add", "-A")
26
- _git(repository_root, "commit", "-m", "seed")
11
+ def _write_pytest_config(directory: Path) -> None:
12
+ """Write a minimal ``pytest.ini`` into *directory*, creating it when absent."""
13
+ directory.mkdir(parents=True, exist_ok=True)
14
+ (directory / "pytest.ini").write_text("[pytest]\n", encoding="utf-8")
27
15
 
28
16
 
29
- def _write_and_stage(repository_root: Path, relative_path: str, file_text: str) -> Path:
17
+ def _write_test_file(repository_root: Path, relative_path: str) -> Path:
18
+ """Write a trivially passing test module at *relative_path* and return its path."""
30
19
  file_path = repository_root / relative_path
31
20
  file_path.parent.mkdir(parents=True, exist_ok=True)
32
- file_path.write_text(file_text, encoding="utf-8")
33
- _git(repository_root, "add", "--", relative_path)
21
+ file_path.write_text(
22
+ "def test_placeholder() -> None:\n assert True\n", encoding="utf-8"
23
+ )
34
24
  return file_path
35
25
 
36
26
 
37
- def _repository_with_root_pytest_config(tmp_path: Path) -> Path:
38
- repository_root = tmp_path / "repo"
39
- repository_root.mkdir()
40
- _init_repository(repository_root)
41
- (repository_root / "pytest.ini").write_text("[pytest]\n", encoding="utf-8")
42
- return repository_root
27
+ def _refuse_to_resolve(monkeypatch: pytest.MonkeyPatch, refused_path: Path) -> None:
28
+ """Make ``Path.resolve`` raise ``OSError`` for *refused_path* and nothing else.
43
29
 
30
+ Stands in for a staged file the filesystem accepts as a file yet refuses to
31
+ resolve, such as a broken junction or a path the user may not traverse.
32
+ """
33
+ original_resolve = Path.resolve
44
34
 
45
- def test_run_staged_test_files_returns_zero_when_nothing_staged(tmp_path: Path) -> None:
46
- repository_root = tmp_path / "repo"
47
- repository_root.mkdir()
48
- _init_repository(repository_root)
35
+ def _resolve_unless_refused(each_path: Path, strict: bool = False) -> Path:
36
+ if each_path == refused_path:
37
+ raise OSError("resolve refused")
38
+ return original_resolve(each_path, strict=strict)
49
39
 
50
- assert staged_test_running.run_staged_test_files(repository_root) == 0
40
+ monkeypatch.setattr(Path, "resolve", _resolve_unless_refused)
51
41
 
52
42
 
53
43
  def test_batched_pytest_arguments_splits_over_the_budget() -> None:
@@ -73,44 +63,87 @@ def test_pytest_target_paths_drops_conftest_and_keeps_real_tests() -> None:
73
63
  assert all_targets == [Path("pkg_a/test_alpha.py")]
74
64
 
75
65
 
76
- def test_run_staged_test_files_returns_zero_when_only_multiple_confests_staged(
66
+ def test_group_staged_tests_by_root_splits_config_less_top_level_directories(
77
67
  tmp_path: Path,
78
68
  ) -> None:
79
- repository_root = _repository_with_root_pytest_config(tmp_path)
80
- _write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
81
- _write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
82
- _write_and_stage(
83
- repository_root, "pkg_c/tests/conftest.py", "import pytest\n"
69
+ repository_root = repository_root_without_pytest_config(tmp_path)
70
+ alpha_test_path = _write_test_file(repository_root, "alpha_package/tests/test_alpha.py")
71
+ beta_test_path = _write_test_file(repository_root, "beta_package/tests/test_beta.py")
72
+
73
+ all_tests_by_root = staged_test_running._group_staged_tests_by_root(
74
+ [alpha_test_path, beta_test_path], repository_root
84
75
  )
85
76
 
86
- assert staged_test_running.run_staged_test_files(repository_root) == 0
77
+ assert all_tests_by_root == {
78
+ (repository_root / "alpha_package").resolve(): [alpha_test_path],
79
+ (repository_root / "beta_package").resolve(): [beta_test_path],
80
+ }
87
81
 
88
82
 
89
- def test_run_staged_test_files_passes_when_confests_stage_with_passing_test(
83
+ def test_group_staged_tests_by_root_places_every_input_in_exactly_one_group(
90
84
  tmp_path: Path,
91
85
  ) -> None:
92
- repository_root = _repository_with_root_pytest_config(tmp_path)
93
- _write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
94
- _write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
95
- _write_and_stage(
96
- repository_root,
97
- "pkg_a/test_alpha.py",
98
- "def test_alpha_passes() -> None:\n assert True\n",
86
+ repository_root = repository_root_without_pytest_config(tmp_path)
87
+ _write_pytest_config(repository_root / "configured_package")
88
+ all_staged_test_paths = [
89
+ _write_test_file(repository_root, "configured_package/suite/test_configured.py"),
90
+ _write_test_file(repository_root, "alpha_package/tests/test_alpha.py"),
91
+ _write_test_file(repository_root, "alpha_package/tests/test_second_alpha.py"),
92
+ _write_test_file(repository_root, "beta_package/test_beta.py"),
93
+ _write_test_file(repository_root, "test_at_repository_root.py"),
94
+ ]
95
+
96
+ all_tests_by_root = staged_test_running._group_staged_tests_by_root(
97
+ all_staged_test_paths, repository_root
99
98
  )
100
99
 
101
- assert staged_test_running.run_staged_test_files(repository_root) == 0
100
+ all_grouped_test_paths = [
101
+ each_path for each_group in all_tests_by_root.values() for each_path in each_group
102
+ ]
103
+ assert sorted(all_grouped_test_paths) == sorted(all_staged_test_paths)
102
104
 
103
105
 
104
- def test_run_staged_test_files_fails_when_real_test_fails_alongside_confests(
106
+ def test_group_staged_tests_by_root_groups_at_the_config_owning_ancestor(
105
107
  tmp_path: Path,
106
108
  ) -> None:
107
- repository_root = _repository_with_root_pytest_config(tmp_path)
108
- _write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
109
- _write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
110
- _write_and_stage(
111
- repository_root,
112
- "pkg_a/test_alpha.py",
113
- "def test_alpha_fails() -> None:\n assert False\n",
109
+ repository_root = repository_root_without_pytest_config(tmp_path)
110
+ configured_package_root = repository_root / "configured_package"
111
+ _write_pytest_config(configured_package_root)
112
+ nested_test_path = _write_test_file(
113
+ repository_root, "configured_package/suite/test_configured.py"
114
+ )
115
+
116
+ all_tests_by_root = staged_test_running._group_staged_tests_by_root(
117
+ [nested_test_path], repository_root
118
+ )
119
+
120
+ assert all_tests_by_root == {configured_package_root.resolve(): [nested_test_path]}
121
+
122
+
123
+ def test_group_staged_tests_by_root_keeps_a_repository_root_file_at_the_root(
124
+ tmp_path: Path,
125
+ ) -> None:
126
+ repository_root = repository_root_without_pytest_config(tmp_path)
127
+ root_level_test_path = _write_test_file(repository_root, "test_at_repository_root.py")
128
+
129
+ all_tests_by_root = staged_test_running._group_staged_tests_by_root(
130
+ [root_level_test_path], repository_root
131
+ )
132
+
133
+ assert all_tests_by_root == {repository_root.resolve(): [root_level_test_path]}
134
+
135
+
136
+ def test_group_staged_tests_by_root_falls_back_to_the_root_when_resolve_raises(
137
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
138
+ ) -> None:
139
+ repository_root = repository_root_without_pytest_config(tmp_path)
140
+ unresolvable_test_path = _write_test_file(
141
+ repository_root, "alpha_package/tests/test_alpha.py"
142
+ )
143
+ _refuse_to_resolve(monkeypatch, unresolvable_test_path)
144
+
145
+ all_tests_by_root = staged_test_running._group_staged_tests_by_root(
146
+ [unresolvable_test_path], repository_root
114
147
  )
115
148
 
116
- assert staged_test_running.run_staged_test_files(repository_root) != 0
149
+ assert all_tests_by_root == {repository_root.resolve(): [unresolvable_test_path]}
@@ -146,3 +146,205 @@ STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
146
146
  MINIMUM_STAGED_PYTEST_PYTHON_MAJOR: int = 3
147
147
 
148
148
  MINIMUM_STAGED_PYTEST_PYTHON_MINOR: int = 12
149
+
150
+ JUNIT_XML_FLAG_PREFIX: str = "--junitxml="
151
+
152
+ JUNIT_XML_TESTCASE_TAG: str = "testcase"
153
+
154
+ JUNIT_XML_FAILURE_TAG: str = "failure"
155
+
156
+ JUNIT_XML_ERROR_TAG: str = "error"
157
+
158
+ JUNIT_XML_CLASSNAME_ATTRIBUTE: str = "classname"
159
+
160
+ JUNIT_XML_NAME_ATTRIBUTE: str = "name"
161
+
162
+ JUNIT_XML_MISSING_ATTRIBUTE_FALLBACK: str = ""
163
+
164
+ REGRESSION_JUNIT_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_junit_"
165
+
166
+ REGRESSION_STAGED_JUNIT_SUBDIRECTORY_NAME: str = "staged"
167
+
168
+ REGRESSION_BASELINE_JUNIT_SUBDIRECTORY_NAME: str = "baseline"
169
+
170
+ REGRESSION_BASELINE_WORKTREE_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_baseline_"
171
+
172
+ REGRESSION_BASELINE_WORKTREE_DIRECTORY_NAME: str = "tree"
173
+
174
+ GIT_HEAD_REVISION: str = "HEAD"
175
+
176
+ ALL_GIT_HEAD_EXISTS_ARGS: tuple[str, ...] = ("rev-parse", "--verify", "HEAD")
177
+
178
+ ALL_GIT_WORKTREE_ADD_DETACH_ARGS: tuple[str, ...] = (
179
+ "worktree",
180
+ "add",
181
+ "--detach",
182
+ "--quiet",
183
+ )
184
+
185
+ ALL_GIT_WORKTREE_REMOVE_FORCE_ARGS: tuple[str, ...] = ("worktree", "remove", "--force")
186
+
187
+ ALL_GIT_WORKTREE_PRUNE_ARGS: tuple[str, ...] = ("worktree", "prune")
188
+
189
+ REGRESSION_NO_BASELINE_MESSAGE: str = (
190
+ "code_rules_gate: no prior commit to compare against (first commit on this branch); "
191
+ "every staged test failure blocks."
192
+ )
193
+
194
+ REGRESSION_WORKTREE_ADD_FAILED_MESSAGE: str = (
195
+ "code_rules_gate: could not open a detached HEAD worktree for the pre-staged baseline "
196
+ "(git worktree add --detach failed); falling back to blocking on every staged test "
197
+ "failure."
198
+ )
199
+
200
+ REGRESSION_WORKTREE_REMOVE_FAILED_MESSAGE: str = (
201
+ "code_rules_gate: git worktree remove --force failed after the baseline check — your "
202
+ "staged changes are untouched in this worktree. Run 'git worktree list' and "
203
+ "'git worktree prune' if a temporary baseline tree remains registered."
204
+ )
205
+
206
+ REGRESSION_PRE_EXISTING_FAILURE_BYPASSED_MESSAGE: str = (
207
+ "code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
208
+ "already present before this change (not caused by it); not blocking."
209
+ )
210
+
211
+ REGRESSION_GROUP_FAILURE_MESSAGE: str = (
212
+ "code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
213
+ "this change introduces; commit blocked."
214
+ )
215
+
216
+ BASELINE_LEAK_PLUGIN_DIRECTORY_NAME: str = "baseline_import_plugin"
217
+
218
+ BASELINE_LEAK_PLUGIN_MODULE_NAME: str = "code_rules_gate_baseline_import_plugin"
219
+
220
+ BASELINE_LEAK_REPORT_FILENAME: str = "imported_from_primary_tree.json"
221
+
222
+ BASELINE_PRIMARY_ROOT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_PRIMARY_ROOT"
223
+
224
+ BASELINE_LEAK_REPORT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_LEAK_REPORT"
225
+
226
+ PYTEST_PLUGINS_ENV_VAR: str = "PYTEST_PLUGINS"
227
+
228
+ PYTEST_PLUGINS_SEPARATOR: str = ","
229
+
230
+ BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS: int = 120
231
+
232
+ BASELINE_IMPORT_PROBE_DEGRADED_TAIL: str = (
233
+ "The HEAD baseline runs with its import roots un-rebased; a module that still "
234
+ "resolves into your working tree is reported after that run and blocks the group."
235
+ )
236
+
237
+ BASELINE_IMPORT_PROBE_TIMED_OUT_MESSAGE: str = (
238
+ "code_rules_gate: the import-root probe did not finish within {seconds} seconds. "
239
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
240
+ )
241
+
242
+ BASELINE_IMPORT_PROBE_UNSTARTABLE_MESSAGE: str = (
243
+ "code_rules_gate: the import-root probe did not start ({reason}). "
244
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
245
+ )
246
+
247
+ BASELINE_IMPORT_PROBE_REJECTED_MESSAGE: str = (
248
+ "code_rules_gate: the import-root probe exited with status {status}. "
249
+ + BASELINE_IMPORT_PROBE_DEGRADED_TAIL
250
+ )
251
+
252
+ PYTHON_INTERPRETER_COMMAND_FLAG: str = "-c"
253
+
254
+ BASELINE_IMPORT_ROOT_PROBE_SOURCE: str = (
255
+ "import json\n"
256
+ "import sys\n"
257
+ "from pathlib import Path\n"
258
+ "all_roots = [each_entry for each_entry in sys.path if each_entry]\n"
259
+ "for each_finder in list(sys.meta_path):\n"
260
+ " owning_module = sys.modules.get(getattr(each_finder, '__module__', '') or '')\n"
261
+ " for each_attribute in ('MAPPING', 'NAMESPACES'):\n"
262
+ " mapping = getattr(owning_module, each_attribute, None)\n"
263
+ " if not isinstance(mapping, dict):\n"
264
+ " continue\n"
265
+ " for each_name, each_target in mapping.items():\n"
266
+ " all_targets = each_target if isinstance(each_target, list) else [each_target]\n"
267
+ " for each_package_directory in all_targets:\n"
268
+ " try:\n"
269
+ " all_roots.append(\n"
270
+ " str(Path(each_package_directory).parents[str(each_name).count('.')])\n"
271
+ " )\n"
272
+ " except (IndexError, TypeError, OSError):\n"
273
+ " continue\n"
274
+ "print(json.dumps(all_roots))\n"
275
+ )
276
+
277
+ BASELINE_LEAK_PLUGIN_SOURCE: str = '''"""Report every module the baseline pytest run imported out of the user's own tree."""
278
+
279
+ import json
280
+ import os
281
+ import sys
282
+ import sysconfig
283
+ from pathlib import Path
284
+
285
+ PRIMARY_ROOT_ENV_VAR = "{primary_root_env_var}"
286
+ REPORT_PATH_ENV_VAR = "{report_path_env_var}"
287
+
288
+
289
+ def _resolved(path_text):
290
+ if not path_text:
291
+ return None
292
+ try:
293
+ return Path(path_text).resolve()
294
+ except OSError:
295
+ return None
296
+
297
+
298
+ def _is_under(candidate, root):
299
+ return candidate == root or root in candidate.parents
300
+
301
+
302
+ def _interpreter_roots():
303
+ all_texts = [sys.prefix, sys.base_prefix, *sysconfig.get_paths().values()]
304
+ all_roots = [_resolved(each_text) for each_text in all_texts]
305
+ return [each_root for each_root in all_roots if each_root is not None]
306
+
307
+
308
+ def _already_reported(report_path):
309
+ try:
310
+ return set(json.loads(report_path.read_text(encoding="utf-8")))
311
+ except (OSError, ValueError):
312
+ return set()
313
+
314
+
315
+ def _imported_from_primary_tree(primary_root, report_path):
316
+ all_interpreter_roots = _interpreter_roots()
317
+ all_leaked = _already_reported(report_path)
318
+ for each_module in list(sys.modules.values()):
319
+ module_path = _resolved(getattr(each_module, "__file__", None))
320
+ if module_path is None or not _is_under(module_path, primary_root):
321
+ continue
322
+ if any(_is_under(module_path, each_root) for each_root in all_interpreter_roots):
323
+ continue
324
+ all_leaked.add(str(module_path))
325
+ return sorted(all_leaked)
326
+
327
+
328
+ def pytest_sessionfinish(session, exitstatus):
329
+ primary_root = _resolved(os.environ.get(PRIMARY_ROOT_ENV_VAR))
330
+ report_path = _resolved(os.environ.get(REPORT_PATH_ENV_VAR))
331
+ if primary_root is None or report_path is None:
332
+ return
333
+ report_path.write_text(
334
+ json.dumps(_imported_from_primary_tree(primary_root, report_path)), encoding="utf-8"
335
+ )
336
+ '''
337
+
338
+ REGRESSION_BASELINE_IMPORT_LEAK_MESSAGE: str = (
339
+ "code_rules_gate: the HEAD baseline run for the group rooted at {group_root} imported "
340
+ "{count} module(s) from your working tree, starting with {first_module}. It measured the "
341
+ "staged code, not HEAD, so that baseline is not trusted and every staged failure in this "
342
+ "group blocks. An editable install whose import hook runs ahead of the path scan is the "
343
+ "usual cause."
344
+ )
345
+
346
+ REGRESSION_BASELINE_LEAK_UNREPORTED_MESSAGE: str = (
347
+ "code_rules_gate: the HEAD baseline run for the group rooted at {group_root} wrote no "
348
+ "import-origin report, so whether it loaded your working-tree code is unknown. That "
349
+ "baseline is not trusted and every staged failure in this group blocks."
350
+ )
@@ -73,10 +73,12 @@ plus the process exit code.
73
73
 
74
74
  `resolve_worker_spawn(...)` takes keyword-only arguments:
75
75
  `role`, `prompt_file`, `working_directory`, `timeout_seconds`,
76
- `is_claude_tier_enabled`, `run_state_directory`, `max_turns`.
76
+ `is_claude_tier_enabled`, `run_state_directory`.
77
77
  `encode_spawn_outcome` turns a `SpawnOutcome` into the same JSON shape the CLI
78
78
  prints.
79
79
 
80
+ The grok tier runs with no turn cap; `timeout_seconds` is its only bound.
81
+
80
82
  ## JSON result shape
81
83
 
82
84
  Stdout is one JSON object:
package/agents/CLAUDE.md CHANGED
@@ -11,7 +11,7 @@ Agent definition files installed into `~/.claude/agents/` by `bin/install.mjs`.
11
11
  | `clean-coder.md` | Clean Coder | Primary code-writing agent; internalizes CODE_RULES.md and targets zero `/check` findings |
12
12
  | `code-advisor.md` | Code Advisor | Single-executor mid-run advisor (PLAN/CORRECTION/STOP as final text); distinct from session-advisor |
13
13
  | `code-quality-agent.md` | Code Quality Agent | Multi-file code quality review across an entire diff or set of files |
14
- | `code-verifier.md` | Code Verifier | Post-hoc verification after coder agents finish; read-only, fresh context, puts the draft verdict through one strongest-tier validation subagent, ends with a fenced verdict |
14
+ | `code-verifier.md` | Code Verifier | Post-hoc verification after coder agents finish; never edits files in the verified tree, with one exception, a deliberate break at an off-tree break site outside it, defined in the agent body; fresh context, puts the draft verdict through one strongest-tier validation subagent, ends with a fenced verdict; `all_pass` true needs a complete shown-red table as well as clean layers |
15
15
  | `deep-research.md` | Deep Research | Citation-grounded research with web search |
16
16
  | `docs-agent.md` | Docs Agent | Documentation authoring and maintenance |
17
17
  | `git-commit-crafter.md` | Git Commit Crafter | Stages changes, writes conventional commit messages, creates commits |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-verifier
3
- description: Post-hoc verification agent for the three-phase code workflow. Spawned by the main session after coder agents finish. Runs every check itself in a fresh context — named gates, tests against recorded baselines, two-way diff-vs-task reading — puts the draft verdict through one strongest-tier validation subagent that tries to refute it, then ends with a fenced verdict block the verifier_verdict_minter hook turns into the commit-gate verdict. Read and execute only; it never edits files.
3
+ description: Post-hoc verification agent for the three-phase code workflow. Spawned by the main session after coder agents finish. Runs every check itself in a fresh context — named gates, tests against recorded baselines, two-way diff-vs-task reading — puts the draft verdict through one strongest-tier validation subagent that tries to refute it, then ends with a fenced verdict block the verifier_verdict_minter hook turns into the commit-gate verdict. Never edits files in the tree under review — its one exception is a deliberate break at an off-tree break site outside that tree, defined in its body.
4
4
  tools: Read, Grep, Glob, Bash, Task
5
5
  color: orange
6
6
  ---
@@ -17,9 +17,10 @@ Run all three layers, in this order:
17
17
 
18
18
  Findings discipline:
19
19
 
20
- - A finding must cite a failing command (with its output) or a named task item. No citation, no finding.
20
+ - A finding must cite a failing command (with its output) or a named task item. No citation, no finding. `findings` carries code defects alone.
21
21
  - Report gaps that affect correctness or the task's stated terms — never style preferences. Sound work produces zero findings; do not invent gaps to look thorough.
22
- - Never edit a file. You verify; repair agents repair.
22
+ - Never edit a file in the work tree you verify — you verify; repair agents repair. The one exception is a deliberate break for the shown-red table, which goes at one of the off-tree break sites the shown-red section below lists.
23
+ - Never run `git stash`. `refs/stash` belongs to the repository, not to a work tree, so every worktree shares one stash list: a `pop` can apply another verifier's entry into your tree and hand you a surface that is not your assignment. To read the base, add a throwaway detached worktree at the base commit (`git worktree add --detach <temp-path> <base-sha>`), read it there, and drop it with `git worktree remove --force <temp-path>`. You only ever need to read a base tree, and stash moves the very tree you were asked to verify.
23
24
  - Never execute code that drives the user's real input or screen — no live mouse moves, keystrokes, clicks, or window focus (pyautogui and its callers included). Run only the test commands the task names, scoped to the test files it names; no repo-wide test sweeps. Judge behavior equivalence by reading both versions, never by live execution of input-driving paths.
24
25
 
25
26
  Before you write the verdict, learn the surface hash of the work tree you verified. Use the branch mode — it resolves the work tree that holds the branch automatically, so it is immune to your own cwd:
@@ -32,14 +33,42 @@ On Windows the same file sits at %USERPROFILE%\.claude\hooks\blocking\verificati
32
33
 
33
34
  The printed hash commits to every changed and untracked file's content in the verified work tree, so it names that surface no matter which directory you or the committer run from. If the CLI prints an empty-surface or wrong-work-tree error and no hash, you are pointed at a work tree with no changes versus origin/main — re-run with the branch mode to locate the correct work tree.
34
35
 
35
- As the last step before the verdict, put your draft verdict through one best-effort strongest-tier validation pass. Spawn a single validation subagent through the Task tool as the `Explore` agent type at the strongest reachable tier: set the Task `subagent_type` to `Explore`, detect the host profile first per `~/.claude/_shared/advisor/advisor-protocol.md` — the source of truth for host detection, the ladder, and its aliases — then on a Claude host pick the strongest reachable tier on the Fable → Opus → Sonnet → Haiku ladder and on a third-party host use the single third-party tier, and set the Task `model:` field to that tier's alias. The `Explore` type carries no Edit or Write tools and cannot spawn further agents, so the harness itself holds the validator to the no-edit, no-spawn contract the next paragraph names. Hand it the draft verdict together with your evidence — every command you ran with its output, and your two-way diff-to-task mapping — and state that its task is adversarial verification of that supplied draft verdict: refute it against the supplied evidence rather than discover code, naming any gate you misread, any task item you mapped wrong, or any finding that does not hold. This pass is always a cold `Explore` spawn, not a message to the session's warm advisor: the refutation needs a grader with no accumulated session context or prior positions, and the verifier runs in sessions that have no advisor bound. When the spawn is unavailable — a Task tool error, an unreachable tier at every rung, or this subagent being barred from spawning further agents — skip the validation pass and emit the draft verdict as it stands, noting the skip in your final message; a spawn failure never blocks the verdict fence from being emitted.
36
+ As the last step before the verdict, put your draft verdict through one best-effort strongest-tier validation pass. Spawn a single validation subagent through the Task tool as the `Explore` agent type at the strongest reachable tier: set the Task `subagent_type` to `Explore`, detect the host profile first per `~/.claude/_shared/advisor/advisor-protocol.md` — the source of truth for host detection, the ladder, and its aliases — then on a Claude host pick the strongest reachable tier on the Fable → Opus → Sonnet → Haiku ladder and on a third-party host use the single third-party tier, and set the Task `model:` field to that tier's alias. A tier denied by policy counts as unreachable, so the walk continues down the ladder to the next tier rather than skipping the validation pass. The `Explore` type carries no Edit or Write tools and cannot spawn further agents, so the harness itself holds the validator to the no-edit, no-spawn contract the next paragraph names. Hand it the draft verdict together with your evidence — every command you ran with its output, your two-way diff-to-task mapping, and the shown-red table with every deliberate-red run labeled as shown-red evidence so the validator reads it as a staged break rather than a genuine failure — and state that its task is adversarial verification of that supplied draft verdict: refute it against the supplied evidence rather than discover code, naming any gate you misread, any task item you mapped wrong, or any finding that does not hold. This pass is always a cold `Explore` spawn, not a message to the session's warm advisor: the refutation needs a grader with no accumulated session context or prior positions, and the verifier runs in sessions that have no advisor bound. Run that spawn synchronously — set the Task `run_in_background` field to `false` — so the validator's reply lands inside this turn. A background spawn returns straight away and its completion notification arrives after your turn is over, so the reply you are waiting for never reaches you and the verdict fence never gets written. When the spawn is unavailable — a Task tool error, an unreachable tier at every rung, or this subagent being barred from spawning further agents — skip the validation pass and emit the draft verdict as it stands, noting the skip in your final message; a spawn failure never blocks the verdict fence from being emitted.
36
37
 
37
- This validation pass is terminal: the `Explore` type gives the validation subagent no way to spawn a further agent or edit a file, so it answers with prose only. When it refutes any part, re-check that part yourself against the commands and the diff, and correct the verdict before you emit it. When it refutes nothing, the draft verdict stands. Then emit the fenced verdict block below it stays the last thing in your message so the verifier_verdict_minter hook reads it.
38
+ Ending your turn without the verdict fence throws the whole run away: every gate you ran and every mapping you built reaches the caller as prose it cannot mint, and the commit gate stays shut on work you already checked. So the fence is unconditional. A validator that returns nothing usable, a tier that never binds, a refutation you accept and fold in each of those ends the same way, with the fence. When you find yourself about to close on a promise to finish once something reports back, run the refutation pass yourself and emit the verdict.
38
39
 
39
- End your final message with exactly one fenced verdict block the verifier_verdict_minter hook parses it, binds it to that hash, and the verified_commit_gate hook unlocks `git commit`/`git push` for any work tree whose live surface matches it:
40
+ This validation pass is terminal: the `Explore` type gives the validation subagent no way to spawn a further agent or edit a file, so it answers with prose only. When it refutes any part, re-check that part yourself against the commands and the diff, and correct the verdict before you emit it. When it refutes nothing, the draft verdict stands. Then write your final message.
41
+
42
+ Your final message runs in one order: the shown-red table, then — only when the verdict is incomplete — the named unshown check, then every `no break available` row named, then the verdict fence last, so the verifier_verdict_minter hook reads it. Every runnable check the verdict rests on gets one row — a runnable check is a layer 1 runnable gate you can execute against the surface. Breakability is not part of that definition: no check leaves the runnable set by being called unbreakable.
43
+
44
+ | Check | Deliberate break | Red | Green |
45
+ |---|---|---|---|
46
+ | `<command you ran>` | `<break you applied>`, or `no break available — <why no input, no environment, and no scratch-copy mutation can make this check fail>`, or `n/a — check not run` | `<exit code or the deciding line>`, or `no red` | `<exit code or the deciding line>` |
47
+
48
+ The Deliberate break cell holds exactly one of those three values: the break you applied, `no break available` with its one-line reason, or `n/a — check not run`. Only the first of the three produced a red, so only the first carries a red result in the Red cell. A `no break available` row and an `n/a — check not run` row each carry the literal `no red` there — never an empty cell, never the Green value repeated, and never an exit code, which would make a row that showed no red scan like one that did.
49
+
50
+ Keep each cell to one line — an exit code, a failing test id, an assert line, or a hook's block message. The Green cell may cite the check's first clean run when you kept that output; a clean result already in hand needs no third run. Longer excerpts go below the table in a plain fenced block carrying no info string.
51
+
52
+ The reading layers, 2 and 3 above, take no rows. Name in prose what you read and what that reading would catch. A runnable check keeps its row whatever you conclude by reading it, and whatever you conclude about breaking it.
53
+
54
+ Break the check at an off-tree break site — a site where the break cannot reach the tree you verify: a failing input or environment fed to the check, or a mutated copy in a scratch directory outside that tree.
55
+
56
+ At either off-tree break site, a check that exercises the changed behavior fails because of the break rather than for an unrelated reason.
57
+
58
+ Break off-tree so the work tree under verification stays as the coders left it; an in-place break moves the surface `manifest_sha256` names and is forbidden. The green is that same check run against the verified tree.
59
+
60
+ Every runnable check the verdict rests on gets a row, with no exclusion path: a rested-on runnable check with no row makes the verdict incomplete, and a check you judge unbreakable still owes its row. Where no runnable check the verdict depends on exists, the surface rests on the reading layers alone and carries an empty table, complete. The empty table is for a surface where nothing runnable exists at all, never for one where a runnable check exists and you skipped it. A row carrying `n/a — check not run` is a runnable check you relied on and never showed red, and it makes the verdict incomplete too; its Red cell carries `no red`.
61
+
62
+ `no break available` is a different claim from `n/a — check not run`: the check ran, and no break exists for it, so its Red cell carries `no red` as well. That row counts complete when it carries the one-line reason naming why no input, no environment, and no scratch-copy mutation can make that check fail, so `all_pass` true stays reachable for a genuinely unbreakable gate. A `no break available` row without that reason is an incomplete row and makes the verdict incomplete exactly as a missing row does.
63
+
64
+ An incomplete verdict names the unshown check directly above the fence and sets `all_pass` to false. Naming a `no break available` row is a separate matter from that incomplete-check naming: a verdict carrying any `no break available` row names each of those rows directly above the fence because the row carries no red, whether the verdict is otherwise complete or incomplete, and naming one never by itself makes the verdict incomplete or sets `all_pass` false. `findings` goes on carrying every code defect the run found, and is empty only when the run found none.
65
+
66
+ Write the table as plain markdown; the fence holds JSON alone.
67
+
68
+ Exactly one fenced verdict block — the verifier_verdict_minter hook parses it, binds it to that hash, and the verified_commit_gate hook unlocks `git commit`/`git push` for any work tree whose live surface matches it:
40
69
 
41
70
  ```verdict
42
71
  {"all_pass": false, "findings": [{"check": "<gate or task item>", "detail": "<command + output, or the named task item and what is missing>"}], "manifest_sha256": "<hash the CLI printed>"}
43
72
  ```
44
73
 
45
- Set `all_pass` to true with an empty `findings` list only when every layer came back clean. Always include `manifest_sha256` so the verdict clears the commit regardless of which work tree the verifier or the committer ran in. Commit-committability gates (CODE_RULES / merge conflicts) must already be green before you are spawned; you are the last semantic check before commit. Any file change after you finish moves that hash and invalidates the verdict.
74
+ Set `all_pass` to true with an empty `findings` list only when every layer came back clean and the shown-red table is complete. Always include `manifest_sha256` so the verdict clears the commit regardless of which work tree the verifier or the committer ran in. Commit-committability gates (CODE_RULES / merge conflicts) must already be green before you are spawned; you are the last semantic check before commit. Any file change after you finish moves that hash and invalidates the verdict.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+
6
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const materializerPath = path.join(packageRoot, "scripts", "codex_compat_materializer.py");
8
+ const bridgePath = path.join(packageRoot, "scripts", "codex_capability_bridge.py");
9
+ const pythonNames = process.platform === "win32" ? ["py", "python"] : ["python3", "python"];
10
+
11
+ export function createHelpText() {
12
+ return [
13
+ "Usage: codex-compat <command> [options]",
14
+ "",
15
+ "Commands:",
16
+ " materialize Plan or apply Claude agents into an explicit Codex root",
17
+ " bridge Translate a Claude capability request to a declarative record",
18
+ "",
19
+ "Materialize options:",
20
+ " --source-root <path> Claude source root (required)",
21
+ " --target-root <path> Codex target root (required)",
22
+ " --apply Write changes; default is dry-run",
23
+ " --allow-prune-all Permit an empty plan to delete every managed file",
24
+ " --python <command> Python interpreter override",
25
+ "",
26
+ "Bridge options:",
27
+ " --surface <name> Capability surface (required)",
28
+ " --payload <json> JSON object payload (required)",
29
+ " --python <command> Python interpreter override",
30
+ "",
31
+ "Python may also be selected with CODEX_COMPAT_PYTHON. Roots are never inferred or written automatically.",
32
+ ].join("\n");
33
+ }
34
+
35
+ function readOption(argumentsList, optionName) {
36
+ const optionIndex = argumentsList.indexOf(optionName);
37
+ if (optionIndex < 0) return undefined;
38
+ return argumentsList[optionIndex + 1];
39
+ }
40
+
41
+ function requireOption(argumentsList, optionName) {
42
+ const optionValue = readOption(argumentsList, optionName);
43
+ if (!optionValue || optionValue.startsWith("--")) throw new Error(`Missing required option: ${optionName}`);
44
+ return optionValue;
45
+ }
46
+
47
+ function writeErrorRecord(errorCode, errorMessage) {
48
+ process.stderr.write(`${JSON.stringify({ error: { code: errorCode, message: errorMessage } })}\n`);
49
+ }
50
+
51
+ export function buildCommand(commandName, argumentsList, interpreter) {
52
+ if (commandName === "materialize") {
53
+ const sourceRoot = requireOption(argumentsList, "--source-root");
54
+ const targetRoot = requireOption(argumentsList, "--target-root");
55
+ const materializerArguments = [materializerPath, sourceRoot, targetRoot];
56
+ if (argumentsList.includes("--apply")) materializerArguments.push("--apply");
57
+ if (argumentsList.includes("--allow-prune-all")) materializerArguments.push("--allow-prune-all");
58
+ return { executable: interpreter, arguments: materializerArguments };
59
+ }
60
+ if (commandName === "bridge") {
61
+ const surface = requireOption(argumentsList, "--surface");
62
+ const payload = requireOption(argumentsList, "--payload");
63
+ return { executable: interpreter, arguments: [bridgePath, surface, payload] };
64
+ }
65
+ throw new Error(`Unknown command: ${commandName}`);
66
+ }
67
+
68
+ export function findPython(commandNames = pythonNames, canRun = (command) => spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0) {
69
+ for (const eachCommand of commandNames) {
70
+ if (canRun(eachCommand)) return eachCommand;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ export function runCommand(command, runChild = spawn) {
76
+ return new Promise((resolve) => {
77
+ const childProcess = runChild(command.executable, command.arguments, { stdio: "inherit" });
78
+ childProcess.on("close", (exitCode, signal) => resolve(exitCode ?? 1 + (signal ? 1 : 0)));
79
+ childProcess.on("error", () => resolve(1));
80
+ });
81
+ }
82
+
83
+ export async function main(argumentsList = process.argv.slice(2), dependencies = {}) {
84
+ if (argumentsList.length === 0 || argumentsList.includes("--help") || argumentsList.includes("-h")) {
85
+ process.stdout.write(`${createHelpText()}\n`);
86
+ return 0;
87
+ }
88
+ const [commandName, ...commandArguments] = argumentsList;
89
+ try {
90
+ const interpreter = readOption(commandArguments, "--python") ?? process.env.CODEX_COMPAT_PYTHON ?? await (dependencies.findPython ?? findPython)();
91
+ if (!interpreter) {
92
+ writeErrorRecord("python_unavailable", "No usable Python interpreter found. Use --python or CODEX_COMPAT_PYTHON.");
93
+ return 1;
94
+ }
95
+ const command = buildCommand(commandName, commandArguments, interpreter);
96
+ return await (dependencies.runCommand ?? runCommand)(command, dependencies.runChild);
97
+ } catch (error) {
98
+ const errorMessage = error instanceof Error ? error.message : "Unknown compatibility command error";
99
+ writeErrorRecord("invalid_command", errorMessage);
100
+ return 2;
101
+ }
102
+ }
103
+
104
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) process.exitCode = await main();