claude-dev-env 2.11.0 → 2.13.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 (203) hide show
  1. package/_shared/advisor/reference/sol-rung.md +5 -2
  2. package/_shared/advisor/reference/warm-up.md +1 -0
  3. package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
  4. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
  5. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  8. package/agents/CLAUDE.md +1 -2
  9. package/agents/pr-description-writer.md +11 -81
  10. package/bin/install.mjs +48 -9
  11. package/bin/install.prune.test.mjs +26 -0
  12. package/bin/install.settings-defaults.test.mjs +60 -33
  13. package/bin/install.test.mjs +2 -4
  14. package/commands/CLAUDE.md +1 -0
  15. package/commands/sr-loop.md +52 -0
  16. package/docs/CLAUDE.md +0 -1
  17. package/docs/references/CLAUDE.md +0 -1
  18. package/hooks/CLAUDE.md +1 -1
  19. package/hooks/blocking/CLAUDE.md +2 -27
  20. package/hooks/blocking/config/CLAUDE.md +1 -13
  21. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  22. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  23. package/hooks/blocking/stop_dispatcher.py +4 -4
  24. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  26. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  27. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  28. package/hooks/diagnostic/CLAUDE.md +3 -3
  29. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  30. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  31. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  32. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  33. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  34. package/hooks/git-hooks/pre_push.py +3 -85
  35. package/hooks/git-hooks/test_pre_push.py +0 -130
  36. package/hooks/hooks.json +0 -35
  37. package/hooks/hooks_constants/CLAUDE.md +0 -9
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  39. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  40. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  41. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  42. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  43. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  46. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  47. package/hooks/lifecycle/CLAUDE.md +1 -3
  48. package/package.json +1 -1
  49. package/rules/CLAUDE.md +0 -2
  50. package/rules/gh-cli-conventions.md +0 -1
  51. package/rules/git-workflow.md +1 -3
  52. package/scripts/CLAUDE.md +0 -1
  53. package/scripts/_code_review_test_support.py +0 -95
  54. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  55. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  56. package/scripts/invoke_code_review.py +8 -451
  57. package/scripts/test_invoke_code_review.py +5 -61
  58. package/scripts/test_invoke_code_review_cli.py +1 -45
  59. package/scripts/test_invoke_code_review_contract.py +1 -118
  60. package/settings.json +1 -10
  61. package/skills/CLAUDE.md +8 -0
  62. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  63. package/skills/autoconverge/reference/convergence.md +7 -7
  64. package/skills/autoconverge/reference/gotchas.md +3 -10
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  66. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  67. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  68. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  69. package/skills/autoconverge/workflow/converge.mjs +201 -238
  70. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  71. package/skills/bugteam/CONSTRAINTS.md +1 -1
  72. package/skills/bugteam/SKILL.md +9 -0
  73. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  74. package/skills/bugteam/test_skill_additions.py +9 -0
  75. package/skills/comments/SKILL.md +65 -0
  76. package/skills/descriptions/SKILL.md +51 -0
  77. package/skills/emergencies/SKILL.md +42 -0
  78. package/skills/imagegen/SKILL.md +21 -0
  79. package/skills/imagegen/scripts/config/__init__.py +1 -0
  80. package/skills/imagegen/scripts/config/constants.py +28 -0
  81. package/skills/imagegen/scripts/imagegen.py +45 -0
  82. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  83. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  84. package/skills/orchestrator/SKILL.md +2 -4
  85. package/skills/plan-to-pr/SKILL.md +6 -7
  86. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  87. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  88. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  89. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  90. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  91. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  92. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  93. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  94. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  95. package/skills/pr-converge/SKILL.md +13 -16
  96. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  97. package/skills/pr-converge/reference/state-schema.md +1 -27
  98. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  99. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  100. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  101. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  102. package/skills/prototype/SKILL.md +4 -4
  103. package/skills/prototype/reference/honest-limitations.md +4 -4
  104. package/skills/prototype/reference/promotion-tasks.md +2 -2
  105. package/skills/prototype/workflows/promotion.md +4 -6
  106. package/skills/reviews/SKILL.md +85 -0
  107. package/skills/small-cl/SKILL.md +52 -0
  108. package/skills/usage-pause/SKILL.md +1 -1
  109. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  110. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  111. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
  112. package/agents/code-verifier.md +0 -74
  113. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  114. package/docs/references/code-review-enforcement.md +0 -132
  115. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  116. package/hooks/blocking/code_review_gate_deny.py +0 -74
  117. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  118. package/hooks/blocking/code_review_push_gate.py +0 -145
  119. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  120. package/hooks/blocking/code_review_stamp_store.py +0 -233
  121. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  122. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  123. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  124. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  125. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  126. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  127. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  128. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  129. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  130. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  131. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  132. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  133. package/hooks/blocking/conftest.py +0 -123
  134. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  135. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  136. package/hooks/blocking/pr_description_body_audit.py +0 -148
  137. package/hooks/blocking/pr_description_command_parser.py +0 -237
  138. package/hooks/blocking/pr_description_enforcer.py +0 -227
  139. package/hooks/blocking/pr_description_pr_number.py +0 -155
  140. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  141. package/hooks/blocking/pr_description_readability.py +0 -364
  142. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  143. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  144. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  145. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  146. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  147. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  148. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  149. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  150. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  151. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  152. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  153. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  154. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  155. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  156. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  157. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  158. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  159. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  160. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  161. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  162. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  163. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  164. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  165. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  166. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  167. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  168. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  169. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  170. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  171. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  172. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  173. package/hooks/blocking/verification_verdict_store.py +0 -1039
  174. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  175. package/hooks/blocking/verified_commit_gate.py +0 -113
  176. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  177. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  178. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  179. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  180. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  181. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  182. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  183. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  184. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  185. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  186. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  187. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  188. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  189. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  190. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  191. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  192. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  193. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  194. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  195. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  196. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  197. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  198. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  199. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  200. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  201. package/rules/proof-of-work-pr-comments.md +0 -22
  202. package/rules/verified-commit-gate-skip.md +0 -4
  203. package/scripts/Show-Asset.ps1 +0 -106
@@ -5,8 +5,11 @@ Open this when `ADVISOR_SOL_XHIGH` is set and a bind is starting.
5
5
 
6
6
  ## Flag
7
7
 
8
- `ADVISOR_SOL_XHIGH=1` (or `true` / `yes` / `on`) opens the rung, set in the environment or by the consuming skill's invocation.
9
- Flag off: the walk starts at the host's Claude ladder, Fable first.
8
+ `ADVISOR_SOL_XHIGH=1` (or `true` / `yes` / `on`) opens the rung. Two channels exist: set the variable in the helper's process environment, or pass `--enable-sol` on the helper invocation — the CLI flag opens the rung for that run without touching the environment.
9
+ Flag off both ways: the walk starts at the host's Claude ladder, Fable first.
10
+ A Windows `setx` write only updates the persisted user environment; only a process started after that write inherits the new value, so an already-running session either sets the flag in its own invoking process environment or passes `--enable-sol`.
11
+
12
+ Every fallback reply carries a `fallback_kind` field: `declined` when policy closed the rung (flag off, usage meter at or below the gate) and `broken` when the Sol path itself failed (missing executable, spawn error, timeout, malformed reply). A `broken` fallback is a defect to report, not a routing outcome.
10
13
 
11
14
  ## Preflight
12
15
 
@@ -25,6 +25,7 @@ State plainly:
25
25
  - Reply via SendMessage to whoever sent the consult, by name — each reply goes back to its own sender, and many different consumers may reach this one agent.
26
26
  - Treat each consult on its own terms, keyed to the sender's stated assignment. Different consumers' consults will interleave in this one transcript — keep each consumer's context separate, and blend only when a consult explicitly asks for that.
27
27
  - If a consult re-raises a question already answered, with nothing new attached, reply by restating the prior answer and naming it as a restatement.
28
+ - Every reply, including this first bind turn, opens its first line with exactly one of the four uppercase signal words — `ENDORSE`, `CORRECTION`, `PLAN`, `STOP` — and nothing else on that line. Standing by after the bind is itself an `ENDORSE` of the charter, stated as that first line.
28
29
 
29
30
  The agent finishes its first turn standing by. `SendMessage` alone resumes it; between consults it waits quietly.
30
31
 
@@ -7,6 +7,7 @@ import importlib
7
7
  import json
8
8
  import math
9
9
  import os
10
+ import shutil
10
11
  import subprocess
11
12
  import sys
12
13
  from collections.abc import Callable, Mapping, Sequence
@@ -20,6 +21,7 @@ if _config_directory_text not in sys.path:
20
21
  sys.path.insert(0, _config_directory_text)
21
22
 
22
23
  from advisor_scripts_constants.sol_advisor_constants import ( # noqa: E402
24
+ ADVISOR_CODEX_EXECUTABLE_ENV_VAR,
23
25
  CODEX_CONFIG_FLAG,
24
26
  CODEX_EXECUTABLE,
25
27
  CODEX_EXEC_SUBCOMMAND,
@@ -35,6 +37,10 @@ from advisor_scripts_constants.sol_advisor_constants import ( # noqa: E402
35
37
  SOL_CODEX_TIMEOUT_REASON,
36
38
  SOL_CODEX_TIMEOUT_SECONDS,
37
39
  SOL_ENV_VAR,
40
+ SOL_ENABLE_FLAG,
41
+ SOL_EXECUTABLE_NOT_FOUND_REASON,
42
+ SOL_FALLBACK_KIND_BROKEN,
43
+ SOL_FALLBACK_KIND_DECLINED,
38
44
  SOL_INVALID_SIGNAL_REASON,
39
45
  SOL_MALFORMED_JSONL_REASON,
40
46
  SOL_MISSING_SESSION_REASON,
@@ -63,6 +69,7 @@ class SolPreflight:
63
69
  eligible: bool
64
70
  percent_left: float | None
65
71
  reason: str
72
+ fallback_kind: str | None = None
66
73
 
67
74
 
68
75
  @dataclass(frozen=True)
@@ -78,17 +85,21 @@ class CodexSolAdvisorReply:
78
85
  sol_enabled: bool
79
86
  selected_tier: str
80
87
  outcome: str
88
+ fallback_kind: str | None
81
89
 
82
90
 
83
91
  def _preflight_fallback(
84
- reason: str, percent_left: float | None
92
+ reason: str,
93
+ percent_left: float | None,
94
+ fallback_kind: str = SOL_FALLBACK_KIND_BROKEN,
85
95
  ) -> SolPreflight:
86
- return SolPreflight(False, percent_left, reason)
96
+ return SolPreflight(False, percent_left, reason, fallback_kind)
87
97
 
88
98
 
89
99
  def _reply_fallback(
90
100
  reason: str,
91
101
  is_sol_enabled: bool,
102
+ fallback_kind: str | None = SOL_FALLBACK_KIND_BROKEN,
92
103
  ) -> CodexSolAdvisorReply:
93
104
  return CodexSolAdvisorReply(
94
105
  session_id=None,
@@ -100,6 +111,7 @@ def _reply_fallback(
100
111
  sol_enabled=is_sol_enabled,
101
112
  selected_tier=ADVISOR_FALLBACK_TIER,
102
113
  outcome=ADVISOR_FALLBACK_RESULT,
114
+ fallback_kind=fallback_kind,
103
115
  )
104
116
 
105
117
 
@@ -118,9 +130,16 @@ def _reply_success(
118
130
  sol_enabled=True,
119
131
  selected_tier=ADVISOR_MODEL_TIER,
120
132
  outcome=CODEX_BIND_SUCCESS_TOKEN,
133
+ fallback_kind=None,
121
134
  )
122
135
 
123
136
 
137
+ def _resolved_setting_by_name(
138
+ setting_by_name: Mapping[str, str] | None,
139
+ ) -> Mapping[str, str]:
140
+ return os.environ if setting_by_name is None else setting_by_name
141
+
142
+
124
143
  def is_sol_advisor_enabled(
125
144
  setting_by_name: Mapping[str, str] | None,
126
145
  ) -> bool:
@@ -132,7 +151,7 @@ def is_sol_advisor_enabled(
132
151
  Returns:
133
152
  Whether the Sol feature flag contains a recognized truthy value.
134
153
  """
135
- resolved_setting_by_name = os.environ if setting_by_name is None else setting_by_name
154
+ resolved_setting_by_name = _resolved_setting_by_name(setting_by_name)
136
155
  return (
137
156
  resolved_setting_by_name.get(SOL_ENV_VAR, "").strip().lower()
138
157
  in ALL_SOL_TRUTHY_VALUES
@@ -223,11 +242,11 @@ def run_sol_preflight(
223
242
  return _preflight_fallback(
224
243
  f"{SOL_PREFLIGHT_FAILURE_REASON}: usage meter is unknown", None
225
244
  )
226
- usage_gate = _load_usage_gate(probe_path)
227
245
  if not usage_gate(percent_left):
228
246
  return _preflight_fallback(
229
247
  f"{SOL_PREFLIGHT_FAILURE_REASON}: usage meter is at or below the gate",
230
248
  percent_left,
249
+ fallback_kind=SOL_FALLBACK_KIND_DECLINED,
231
250
  )
232
251
  except subprocess.TimeoutExpired as probe_error:
233
252
  return _preflight_fallback(f"{SOL_PROBE_TIMEOUT_REASON}: {probe_error}", None)
@@ -245,17 +264,44 @@ def run_sol_preflight(
245
264
  return SolPreflight(True, percent_left, "usage meter is above the Sol gate")
246
265
 
247
266
 
248
- def build_codex_arguments(session_id: str | None = None) -> list[str]:
267
+ def resolve_codex_executable(
268
+ setting_by_name: Mapping[str, str] | None,
269
+ ) -> str | None:
270
+ """Resolve the Codex CLI executable to an invocable name or path.
271
+
272
+ A bare "codex" name fails Windows `CreateProcess`, since the npm shim
273
+ directory holds only `codex` (a sh script), `codex.cmd`, and `codex.ps1`.
274
+ `shutil.which` finds `codex.cmd` via `PATHEXT`. An explicit override
275
+ always wins and is trusted without a `which` check.
276
+
277
+ Args:
278
+ setting_by_name: Optional environment-like settings mapping.
279
+
280
+ Returns:
281
+ An invocable executable name or path, or None when unresolved.
282
+ """
283
+ resolved_setting_by_name = _resolved_setting_by_name(setting_by_name)
284
+ executable_override = resolved_setting_by_name.get(ADVISOR_CODEX_EXECUTABLE_ENV_VAR, "").strip()
285
+ if executable_override:
286
+ return executable_override
287
+ return shutil.which(CODEX_EXECUTABLE)
288
+
289
+
290
+ def build_codex_arguments(
291
+ codex_executable: str,
292
+ session_id: str | None = None,
293
+ ) -> list[str]:
249
294
  """Build the installed CLI's shell-free bind or resume argv.
250
295
 
251
296
  Args:
297
+ codex_executable: Resolved executable name or path to invoke.
252
298
  session_id: Optional existing session to resume.
253
299
 
254
300
  Returns:
255
301
  The shell-free Codex command argument vector.
256
302
  """
257
303
  command_arguments = [
258
- CODEX_EXECUTABLE,
304
+ codex_executable,
259
305
  CODEX_EXEC_SUBCOMMAND,
260
306
  CODEX_MODEL_FLAG,
261
307
  ADVISOR_CODEX_MODEL_ID,
@@ -333,6 +379,21 @@ def parse_codex_jsonl_reply(
333
379
  return _reply_success(discovered_session_id, final_guidance, guidance_signal)
334
380
 
335
381
 
382
+ def _resolve_sol_preflight(
383
+ preflight: SolPreflight | None,
384
+ probe_path: Path | None,
385
+ process_runner: Callable[..., subprocess.CompletedProcess[str]],
386
+ ) -> SolPreflight:
387
+ if preflight is not None:
388
+ return preflight
389
+ resolved_probe_path = (
390
+ resolve_usage_probe_path(Path.home()) if probe_path is None else probe_path
391
+ )
392
+ return run_sol_preflight(
393
+ probe_path=resolved_probe_path, process_runner=process_runner
394
+ )
395
+
396
+
336
397
  def run_codex_sol_advisor(
337
398
  prompt: str,
338
399
  working_directory: Path,
@@ -356,26 +417,25 @@ def run_codex_sol_advisor(
356
417
  Returns:
357
418
  The parsed Sol guidance or an explicit Fable fallback reply.
358
419
  """
359
- is_sol_enabled = is_sol_advisor_enabled(setting_by_name)
360
- if not is_sol_enabled:
361
- return _reply_fallback("Sol advisor flag is disabled", False)
362
- resolved_preflight = (
363
- run_sol_preflight(
364
- probe_path=(
365
- resolve_usage_probe_path(Path.home())
366
- if probe_path is None
367
- else probe_path
368
- ),
369
- process_runner=process_runner,
420
+ if not is_sol_advisor_enabled(setting_by_name):
421
+ return _reply_fallback(
422
+ "Sol advisor flag is disabled",
423
+ False,
424
+ fallback_kind=SOL_FALLBACK_KIND_DECLINED,
370
425
  )
371
- if preflight is None
372
- else preflight
373
- )
426
+ codex_executable = resolve_codex_executable(setting_by_name)
427
+ if codex_executable is None:
428
+ return _reply_fallback(SOL_EXECUTABLE_NOT_FOUND_REASON, True)
429
+ resolved_preflight = _resolve_sol_preflight(preflight, probe_path, process_runner)
374
430
  if not resolved_preflight.eligible:
375
- return _reply_fallback(resolved_preflight.reason, is_sol_enabled)
431
+ return _reply_fallback(
432
+ resolved_preflight.reason,
433
+ True,
434
+ fallback_kind=resolved_preflight.fallback_kind,
435
+ )
376
436
  try:
377
437
  completed_process = process_runner(
378
- build_codex_arguments(session_id=session_id),
438
+ build_codex_arguments(codex_executable, session_id=session_id),
379
439
  cwd=str(working_directory),
380
440
  input=prompt,
381
441
  capture_output=True,
@@ -385,22 +445,18 @@ def run_codex_sol_advisor(
385
445
  timeout=SOL_CODEX_TIMEOUT_SECONDS,
386
446
  )
387
447
  except subprocess.TimeoutExpired as bind_error:
388
- return _reply_fallback(
389
- f"{SOL_CODEX_TIMEOUT_REASON}: {bind_error}", is_sol_enabled
390
- )
448
+ return _reply_fallback(f"{SOL_CODEX_TIMEOUT_REASON}: {bind_error}", True)
391
449
  except (OSError, subprocess.SubprocessError) as bind_error:
392
- return _reply_fallback(
393
- f"{SOL_BIND_FAILURE_REASON}: {bind_error}", is_sol_enabled
394
- )
450
+ return _reply_fallback(f"{SOL_BIND_FAILURE_REASON}: {bind_error}", True)
395
451
  if completed_process.returncode != 0:
396
452
  return _reply_fallback(
397
453
  f"{SOL_BIND_FAILURE_REASON}: process exit {completed_process.returncode}",
398
- is_sol_enabled,
454
+ True,
399
455
  )
400
456
  return parse_codex_jsonl_reply(
401
457
  completed_process.stdout,
402
458
  existing_session_id=session_id,
403
- is_sol_enabled=is_sol_enabled,
459
+ is_sol_enabled=True,
404
460
  )
405
461
 
406
462
 
@@ -417,6 +473,12 @@ def build_argument_parser() -> argparse.ArgumentParser:
417
473
  mode_group.add_argument("--bind", action="store_true")
418
474
  mode_group.add_argument("--resume", metavar=SOL_SESSION_ID_METAVAR)
419
475
  argument_parser.add_argument("--cwd", required=True, type=Path)
476
+ argument_parser.add_argument(
477
+ SOL_ENABLE_FLAG,
478
+ dest="is_sol_requested",
479
+ action="store_true",
480
+ help="Open the Sol rung for this invocation without an environment flag.",
481
+ )
420
482
  return argument_parser
421
483
 
422
484
 
@@ -430,12 +492,15 @@ def main(all_cli_arguments: Sequence[str]) -> int:
430
492
  Zero for a successful Sol response, or one for an explicit fallback.
431
493
  """
432
494
  parsed_arguments = build_argument_parser().parse_args(list(all_cli_arguments))
495
+ setting_by_name: Mapping[str, str] = os.environ
496
+ if parsed_arguments.is_sol_requested:
497
+ setting_by_name = {**os.environ, SOL_ENV_VAR: "1"}
433
498
  advisor_reply = run_codex_sol_advisor(
434
499
  prompt=sys.stdin.read(),
435
500
  working_directory=parsed_arguments.cwd,
436
501
  preflight=None,
437
502
  probe_path=None,
438
- setting_by_name=os.environ,
503
+ setting_by_name=setting_by_name,
439
504
  session_id=parsed_arguments.resume if not parsed_arguments.bind else None,
440
505
  process_runner=subprocess.run,
441
506
  )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  CODEX_EXECUTABLE: str = "codex"
6
+ ADVISOR_CODEX_EXECUTABLE_ENV_VAR: str = "ADVISOR_CODEX_EXECUTABLE"
6
7
  CODEX_READ_ONLY_SANDBOX: str = "read-only"
7
8
  CODEX_JSON_FLAG: str = "--json"
8
9
  CODEX_MODEL_FLAG: str = "--model"
@@ -26,3 +27,7 @@ SOL_CODEX_TIMEOUT_REASON: str = "Codex Sol xhigh request timed out"
26
27
  SOL_MALFORMED_JSONL_REASON: str = "Codex Sol xhigh returned malformed JSONL"
27
28
  SOL_MISSING_SESSION_REASON: str = "Codex Sol xhigh returned no session id"
28
29
  SOL_INVALID_SIGNAL_REASON: str = "Codex Sol xhigh returned an invalid guidance signal"
30
+ SOL_EXECUTABLE_NOT_FOUND_REASON: str = "Codex Sol xhigh could not find the codex executable on PATH"
31
+ SOL_FALLBACK_KIND_DECLINED: str = "declined"
32
+ SOL_FALLBACK_KIND_BROKEN: str = "broken"
33
+ SOL_ENABLE_FLAG: str = "--enable-sol"
@@ -37,6 +37,17 @@ USAGE_PROBE_PATH = (
37
37
  / "scripts"
38
38
  / "codex_usage_probe.py"
39
39
  )
40
+ WINDOWS_SHIM_PATH = r"C:\Users\me\AppData\Roaming\npm\codex.cmd"
41
+ ENABLED_SETTING_BY_NAME = {sol_advisor.SOL_ENV_VAR: "1"}
42
+
43
+
44
+ @pytest.fixture(autouse=True)
45
+ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
46
+ monkeypatch.setattr(
47
+ sol_advisor.shutil,
48
+ "which",
49
+ lambda name: "codex" if name == sol_advisor.CODEX_EXECUTABLE else None,
50
+ )
40
51
 
41
52
 
42
53
  def _probe_process(
@@ -130,6 +141,7 @@ def test_main_serializes_stable_result_field(monkeypatch: pytest.MonkeyPatch, ca
130
141
  sol_enabled=True,
131
142
  selected_tier=sol_advisor.ADVISOR_MODEL_TIER,
132
143
  outcome=sol_advisor.CODEX_BIND_SUCCESS_TOKEN,
144
+ fallback_kind=None,
133
145
  ),
134
146
  )
135
147
  monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
@@ -155,18 +167,165 @@ def test_bind_and_resume_arguments_match_installed_codex_interface() -> None:
155
167
  "read-only",
156
168
  "--json",
157
169
  ]
158
- assert sol_advisor.build_codex_arguments() == [
170
+ assert sol_advisor.build_codex_arguments("codex") == [
159
171
  *expected_common_arguments,
160
172
  "-",
161
173
  ]
162
- assert sol_advisor.build_codex_arguments("thread-1") == [
163
- *expected_common_arguments,
174
+ assert sol_advisor.build_codex_arguments(
175
+ WINDOWS_SHIM_PATH, session_id="thread-1"
176
+ ) == [
177
+ WINDOWS_SHIM_PATH,
178
+ *expected_common_arguments[1:],
164
179
  "resume",
165
180
  "thread-1",
166
181
  "-",
167
182
  ]
168
183
 
169
184
 
185
+ def test_resolve_codex_executable_prefers_the_env_var_override(
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ ) -> None:
188
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
189
+
190
+ resolved_executable = sol_advisor.resolve_codex_executable(
191
+ {sol_advisor.ADVISOR_CODEX_EXECUTABLE_ENV_VAR: WINDOWS_SHIM_PATH}
192
+ )
193
+
194
+ assert resolved_executable == WINDOWS_SHIM_PATH
195
+
196
+
197
+ def test_resolve_codex_executable_falls_back_to_which_search(
198
+ monkeypatch: pytest.MonkeyPatch,
199
+ ) -> None:
200
+ monkeypatch.setattr(
201
+ sol_advisor.shutil,
202
+ "which",
203
+ lambda name: (
204
+ WINDOWS_SHIM_PATH if name == sol_advisor.CODEX_EXECUTABLE else None
205
+ ),
206
+ )
207
+
208
+ resolved_executable = sol_advisor.resolve_codex_executable({})
209
+
210
+ assert resolved_executable == WINDOWS_SHIM_PATH
211
+
212
+
213
+ def test_resolve_codex_executable_returns_none_when_unresolved(
214
+ monkeypatch: pytest.MonkeyPatch,
215
+ ) -> None:
216
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
217
+
218
+ resolved_executable = sol_advisor.resolve_codex_executable({})
219
+
220
+ assert resolved_executable is None
221
+
222
+
223
+ def test_bind_falls_back_with_a_clear_reason_when_executable_is_missing(
224
+ monkeypatch: pytest.MonkeyPatch,
225
+ ) -> None:
226
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
227
+ calls: list[list[str]] = []
228
+
229
+ def process_runner(
230
+ arguments: list[str], **kwargs: object
231
+ ) -> subprocess.CompletedProcess[str]:
232
+ calls.append(arguments)
233
+ return _probe_process({"percent_left": 90})
234
+
235
+ reply = sol_advisor.run_codex_sol_advisor(
236
+ prompt="first consult",
237
+ working_directory=Path("."),
238
+ preflight=None,
239
+ probe_path=USAGE_PROBE_PATH,
240
+ session_id=None,
241
+ process_runner=process_runner,
242
+ setting_by_name={sol_advisor.SOL_ENV_VAR: "1"},
243
+ )
244
+
245
+ assert not reply.successful
246
+ assert reply.is_fallback
247
+ assert reply.reason is not None
248
+ assert "codex" in reply.reason
249
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
250
+ assert calls == []
251
+
252
+
253
+ def test_policy_fallbacks_are_marked_declined() -> None:
254
+ def gate_closed_runner(
255
+ arguments: list[str], **kwargs: object
256
+ ) -> subprocess.CompletedProcess[str]:
257
+ return _probe_process({"percent_left": 5})
258
+
259
+ disabled_reply = sol_advisor.run_codex_sol_advisor(
260
+ prompt="first consult",
261
+ working_directory=Path("."),
262
+ preflight=None,
263
+ probe_path=USAGE_PROBE_PATH,
264
+ session_id=None,
265
+ process_runner=gate_closed_runner,
266
+ setting_by_name={},
267
+ )
268
+ gate_closed_reply = sol_advisor.run_codex_sol_advisor(
269
+ prompt="first consult",
270
+ working_directory=Path("."),
271
+ preflight=None,
272
+ probe_path=USAGE_PROBE_PATH,
273
+ session_id=None,
274
+ process_runner=gate_closed_runner,
275
+ setting_by_name=ENABLED_SETTING_BY_NAME,
276
+ )
277
+
278
+ assert disabled_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
279
+ assert gate_closed_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
280
+
281
+
282
+ def test_probe_failure_fallback_is_marked_broken() -> None:
283
+ def failing_probe_runner(
284
+ arguments: list[str], **kwargs: object
285
+ ) -> subprocess.CompletedProcess[str]:
286
+ return _probe_process({}, returncode=3)
287
+
288
+ reply = sol_advisor.run_codex_sol_advisor(
289
+ prompt="first consult",
290
+ working_directory=Path("."),
291
+ preflight=None,
292
+ probe_path=USAGE_PROBE_PATH,
293
+ session_id=None,
294
+ process_runner=failing_probe_runner,
295
+ setting_by_name=ENABLED_SETTING_BY_NAME,
296
+ )
297
+
298
+ assert reply.is_fallback
299
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
300
+
301
+
302
+ def test_enable_sol_flag_opens_the_rung_without_an_environment_flag(
303
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
304
+ ) -> None:
305
+ captured_settings: dict[str, str] = {}
306
+
307
+ def fake_advisor(**kwargs: object) -> object:
308
+ captured_settings.update(dict(kwargs["setting_by_name"])) # type: ignore[arg-type]
309
+ return sol_advisor._reply_fallback(
310
+ "probe declined",
311
+ True,
312
+ fallback_kind=sol_advisor.SOL_FALLBACK_KIND_DECLINED,
313
+ )
314
+
315
+ monkeypatch.setattr(sol_advisor, "run_codex_sol_advisor", fake_advisor)
316
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
317
+ monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
318
+
319
+ exit_code = sol_advisor.main(
320
+ ["--bind", "--cwd", ".", sol_advisor.SOL_ENABLE_FLAG]
321
+ )
322
+ payload = json.loads(capsys.readouterr().out)
323
+
324
+ assert exit_code == 1
325
+ assert captured_settings[sol_advisor.SOL_ENV_VAR] == "1"
326
+ assert payload["fallback_kind"] == sol_advisor.SOL_FALLBACK_KIND_DECLINED
327
+
328
+
170
329
  def test_successful_probe_requires_finite_meter_above_configured_gate() -> None:
171
330
  def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
172
331
  return _probe_process({"percent_left": 90})
@@ -304,12 +463,12 @@ def test_bind_runs_probe_then_codex_with_read_only_xhigh_settings() -> None:
304
463
  probe_path=USAGE_PROBE_PATH,
305
464
  session_id=None,
306
465
  process_runner=process_runner,
307
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
466
+ setting_by_name=ENABLED_SETTING_BY_NAME,
308
467
  )
309
468
 
310
469
  assert reply.successful
311
470
  assert len(calls) == 2
312
- assert calls[1][0] == sol_advisor.build_codex_arguments()
471
+ assert calls[1][0] == sol_advisor.build_codex_arguments("codex")
313
472
  assert calls[1][1]["cwd"] == "."
314
473
  assert calls[1][1]["shell"] is False
315
474
  assert calls[1][1]["timeout"]
@@ -331,7 +490,7 @@ def test_team_advisor_path_preserves_sol_routing_fields() -> None:
331
490
  probe_path=USAGE_PROBE_PATH,
332
491
  session_id=None,
333
492
  process_runner=_two_step_process_runner(calls, guidance="PLAN\ninspect"),
334
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
493
+ setting_by_name=ENABLED_SETTING_BY_NAME,
335
494
  )
336
495
 
337
496
  assert reply.successful
@@ -358,7 +517,7 @@ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
358
517
  probe_path=USAGE_PROBE_PATH,
359
518
  session_id=None,
360
519
  process_runner=process_runner,
361
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
520
+ setting_by_name=ENABLED_SETTING_BY_NAME,
362
521
  )
363
522
 
364
523
  assert not reply.successful
@@ -369,7 +528,10 @@ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
369
528
  assert len(calls) == 1
370
529
 
371
530
 
372
- def test_disabled_flag_uses_default_optional_routing_inputs() -> None:
531
+ def test_disabled_flag_uses_default_optional_routing_inputs(
532
+ monkeypatch: pytest.MonkeyPatch,
533
+ ) -> None:
534
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
373
535
  reply = sol_advisor.run_codex_sol_advisor(
374
536
  prompt="first consult",
375
537
  working_directory=Path("."),
@@ -398,13 +560,13 @@ def test_resume_runs_the_usage_gate_before_codex() -> None:
398
560
  session_id="thread-1",
399
561
  probe_path=USAGE_PROBE_PATH,
400
562
  process_runner=_two_step_process_runner(calls, guidance="ENDORSE\nready"),
401
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
563
+ setting_by_name=ENABLED_SETTING_BY_NAME,
402
564
  )
403
565
 
404
566
  assert reply.successful
405
567
  assert calls == [
406
568
  [sys.executable, str(USAGE_PROBE_PATH)],
407
- sol_advisor.build_codex_arguments("thread-1"),
569
+ sol_advisor.build_codex_arguments("codex", session_id="thread-1"),
408
570
  ]
409
571
 
410
572
 
@@ -465,7 +627,7 @@ def test_codex_failure_modes_always_return_fallback(
465
627
  probe_path=USAGE_PROBE_PATH,
466
628
  session_id=None,
467
629
  process_runner=process_runner,
468
- setting_by_name={"ADVISOR_SOL_XHIGH": "1"},
630
+ setting_by_name=ENABLED_SETTING_BY_NAME,
469
631
  )
470
632
 
471
633
  assert not reply.successful
@@ -116,9 +116,8 @@ def _report_partitioned_violations(
116
116
  ) -> int:
117
117
  """Print the violation sections and return the gate exit code.
118
118
 
119
- Importers outside this directory (the code-verifier spawn-preflight hook)
120
- call this surface with the partition spread across positional arguments,
121
- so this wrapper keeps that calling shape and folds the pieces into the
119
+ External callers pass the partition across positional arguments, so this
120
+ wrapper keeps that calling shape and folds the pieces into the
122
121
  ``PartitionedViolations`` tuple the gate-running module consumes.
123
122
 
124
123
  Args:
@@ -250,29 +250,3 @@ def _gate_import_names_in_source(module_source: str) -> set[str]:
250
250
  for each_line in import_block.splitlines()
251
251
  if each_line.strip() and not each_line.strip().startswith("#")
252
252
  }
253
-
254
-
255
- def test_entry_exposes_every_symbol_the_preflight_hook_imports() -> None:
256
- """The gate entry re-exports the whole surface the spawn-preflight hook uses.
257
-
258
- The code-verifier spawn-preflight hook imports gate helpers by name at
259
- runtime; a name the entry drops breaks that hook's import and silently
260
- turns its deny paths into allows.
261
- """
262
- hook_path = (
263
- Path(__file__).resolve().parents[3]
264
- / "hooks"
265
- / "blocking"
266
- / "code_verifier_spawn_preflight_gate.py"
267
- )
268
- hook_imported_names = _gate_import_names_in_source(
269
- hook_path.read_text(encoding="utf-8")
270
- )
271
-
272
- assert hook_imported_names, "hook no longer imports from code_rules_gate"
273
- missing_names = sorted(
274
- each_name
275
- for each_name in hook_imported_names
276
- if not hasattr(gate_module, each_name)
277
- )
278
- assert not missing_names, f"gate entry drops hook imports: {missing_names}"
package/agents/CLAUDE.md CHANGED
@@ -11,13 +11,12 @@ Agent definition files installed into `~/.claude/agents/` by `bin/install.mjs`.
11
11
  | `clean-coder.md` | Clean Coder | Primary code-writing agent; links AGENTS.md / CODE_RULES / enforcer; task-local discovery and gate-clean first writes |
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, run only when the task owner asks for a pass; 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
14
  | `deep-research.md` | Deep Research | Citation-grounded research with web search |
16
15
  | `docs-agent.md` | Docs Agent | Documentation authoring and maintenance |
17
16
  | `git-commit-crafter.md` | Git Commit Crafter | Stages changes, writes conventional commit messages, creates commits |
18
17
  | `issue-tracker.md` | Issue Tracker | Primary handler for one GitHub issue action per spawn; loads the issue-tracker skill (plain-brief); returns issue numbers and URLs |
19
18
  | `plan-packet-validator.md` | Plan Packet Validator | Fresh-context validator for workflow-generated plan packets under `docs/plans/` |
20
- | `pr-description-writer.md` | PR Description Writer | Authors PR descriptions in Anthropic-style shapes that pass the `pr_description_enforcer` hook's body audit |
19
+ | `pr-description-writer.md` | PR Description Writer | Drafts PR descriptions and comments from the current diff using the canonical description and comment guides |
21
20
  | `session-advisor.md` | Session Advisor | Standing multi-consumer reviewer; SendMessage only; returns endorse/correction/plan/stop |
22
21
  | `skill-writer-agent.md` | Skill Writer Agent | Authors SKILL.md and companion files to skill-builder conventions; caller-agnostic authoring specialist |
23
22