claude-dev-env 2.3.0 → 2.4.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 (136) hide show
  1. package/CLAUDE.md +3 -2
  2. package/agents/CLAUDE.md +1 -0
  3. package/agents/code-verifier.md +3 -3
  4. package/agents/skill-writer-agent.md +84 -0
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
  6. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  7. package/docs/CLAUDE.md +1 -0
  8. package/docs/references/CLAUDE.md +1 -0
  9. package/docs/references/code-review-enforcement.md +97 -0
  10. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  11. package/hooks/blocking/CLAUDE.md +8 -1
  12. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  13. package/hooks/blocking/code_review_gate_deny.py +74 -0
  14. package/hooks/blocking/code_review_pr_create_gate.py +194 -0
  15. package/hooks/blocking/code_review_push_gate.py +140 -0
  16. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
  17. package/hooks/blocking/code_review_stamp_store.py +233 -0
  18. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  19. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  20. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  21. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  22. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  23. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  24. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  25. package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
  26. package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
  27. package/hooks/blocking/config/verified_commit_constants.py +13 -9
  28. package/hooks/blocking/conftest.py +2 -0
  29. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  30. package/hooks/blocking/destructive_command_blocker.py +19 -6
  31. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  33. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  34. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  35. package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
  36. package/hooks/blocking/test_code_review_push_gate.py +189 -0
  37. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
  38. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  39. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  40. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  41. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  42. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  43. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  44. package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
  45. package/hooks/blocking/test_verification_verdict_store.py +920 -903
  46. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  47. package/hooks/blocking/verification_verdict_store.py +27 -5
  48. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  49. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  50. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  51. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  52. package/hooks/git-hooks/pre_push.py +89 -2
  53. package/hooks/git-hooks/test_pre_push.py +103 -0
  54. package/hooks/hooks.json +16 -1
  55. package/hooks/hooks_constants/CLAUDE.md +1 -0
  56. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  57. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  58. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  59. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  60. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  61. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  62. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  63. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  64. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  65. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  66. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  67. package/hooks/validators/CLAUDE.md +1 -0
  68. package/hooks/validators/mypy_integration.py +63 -50
  69. package/hooks/validators/pyproject_config_discovery.py +101 -0
  70. package/hooks/validators/ruff_integration.py +211 -23
  71. package/hooks/validators/run_all_validators.py +21 -14
  72. package/hooks/validators/test_mypy_integration.py +32 -0
  73. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  74. package/hooks/validators/test_ruff_integration.py +68 -1
  75. package/hooks/validators/test_run_all_validators.py +25 -0
  76. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  77. package/package.json +1 -1
  78. package/rules/docstring-prose-matches-implementation.md +20 -43
  79. package/rules/durable-post-artifacts.md +7 -0
  80. package/scripts/codec_forwarding_test_support.py +83 -0
  81. package/scripts/conftest.py +8 -0
  82. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  83. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
  84. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  85. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  86. package/scripts/invoke_code_review.py +550 -38
  87. package/scripts/resolve_worker_spawn.py +8 -1
  88. package/scripts/test_invoke_code_review.py +298 -4
  89. package/scripts/test_invoke_code_review_codec.py +77 -0
  90. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  91. package/skills/autoconverge/SKILL.md +9 -3
  92. package/skills/autoconverge/reference/convergence.md +2 -1
  93. package/skills/autoconverge/reference/multi-pr.md +6 -1
  94. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  95. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
  96. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
  98. package/skills/autoconverge/workflow/converge.mjs +24 -8
  99. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  100. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  101. package/skills/fresh-branch/CLAUDE.md +2 -0
  102. package/skills/fresh-branch/SKILL.md +2 -0
  103. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  104. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  105. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  106. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  107. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  108. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  109. package/skills/prototype/SKILL.md +86 -0
  110. package/skills/prototype/reference/honest-limitations.md +23 -0
  111. package/skills/prototype/reference/promotion-tasks.md +23 -0
  112. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  113. package/skills/prototype/scripts/conftest.py +15 -0
  114. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  115. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  116. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  117. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  118. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  119. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  120. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  121. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  122. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  123. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  124. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  125. package/skills/prototype/workflows/promotion.md +27 -0
  126. package/skills/prototype/workflows/sandbox.md +35 -0
  127. package/skills/skill-builder/CLAUDE.md +3 -3
  128. package/skills/skill-builder/SKILL.md +5 -5
  129. package/skills/skill-builder/references/CLAUDE.md +1 -1
  130. package/skills/skill-builder/references/delegation-map.md +3 -3
  131. package/skills/skill-builder/references/description-field.md +1 -1
  132. package/skills/skill-builder/references/skill-modularity.md +2 -3
  133. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  134. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  135. package/skills/skill-builder/workflows/new-skill.md +2 -2
  136. package/skills/team-advisor/SKILL.md +2 -2
@@ -15,20 +15,19 @@ the spawn does not wait for interactive input. Result JSON on stdout only::
15
15
 
16
16
  {"mode", "served_command", "returncode", "dirty_tree"}
17
17
 
18
- A clean stamp requires a successful serve (``returncode == 0``, and for chain
19
- mode a non-null ``served_command``) plus ``dirty_tree`` false. A failed chain
20
- or config/host error leaves the tree clean but must not advance past
21
- CODE_REVIEW — use ``is_code_review_clean_stamp_allowed``.
18
+ ``--record-stamp`` forces chain mode, loops a capped number of review passes,
19
+ and mints a clean stamp only when a pass exits 0 with a stable surface hash.
22
20
 
23
21
  Import ``invoke_code_review`` for the outcome object, or run as a CLI::
24
22
 
25
23
  python invoke_code_review.py --cwd <dir> --session-model <alias>
26
- [--timeout-seconds N]
24
+ [--timeout-seconds N] [--record-stamp] [effort]
27
25
  """
28
26
 
29
27
  from __future__ import annotations
30
28
 
31
29
  import argparse
30
+ import importlib.util
32
31
  import json
33
32
  import subprocess
34
33
  import sys
@@ -36,6 +35,7 @@ import threading
36
35
  from collections.abc import Callable, Sequence
37
36
  from dataclasses import dataclass
38
37
  from pathlib import Path
38
+ from types import ModuleType
39
39
 
40
40
  if str(Path(__file__).resolve().parent) not in sys.path:
41
41
  sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -57,24 +57,47 @@ from claude_chain_runner import ( # noqa: E402
57
57
  )
58
58
  from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
59
59
  CHAIN_CONFIG_ERROR_EXIT_CODE,
60
+ collect_forwarded_text_codec,
60
61
  )
61
62
  from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
63
+ ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER,
64
+ CLI_EFFORT_HELP,
65
+ CLI_EFFORT_METAVAR,
66
+ CLI_RECORD_STAMP_HELP,
62
67
  CLI_SESSION_MODEL_FLAG,
68
+ CODE_REVIEW_FIX_FLAG,
63
69
  CODE_REVIEW_MODEL_ALIAS,
64
- CODE_REVIEW_PROMPT,
70
+ CODE_REVIEW_SLASH_COMMAND,
71
+ DEFAULT_CODE_REVIEW_EFFORT,
72
+ EFFORT_TOKEN_LIST_SEPARATOR,
65
73
  GIT_BINARY,
66
74
  GIT_PORCELAIN_FLAG,
67
75
  GIT_STATUS_SUBCOMMAND,
68
76
  HOST_PROFILE_ERROR_RETURNCODE,
69
77
  IN_SESSION_RETURNCODE,
78
+ INVALID_EFFORT_MESSAGE,
79
+ INVALID_EFFORT_RETURNCODE,
80
+ MAXIMUM_STAMP_MINT_PASSES,
70
81
  MODE_CHAIN,
71
82
  MODE_IN_SESSION,
72
83
  PERMISSION_MODE_BYPASS,
73
84
  PERMISSION_MODE_FLAG,
85
+ RECORD_STAMP_FLAG,
86
+ RESULT_KEY_BOUND_HASH,
74
87
  RESULT_KEY_DIRTY_TREE,
75
88
  RESULT_KEY_MODE,
89
+ RESULT_KEY_PASS_COUNT,
76
90
  RESULT_KEY_RETURNCODE,
77
91
  RESULT_KEY_SERVED_COMMAND,
92
+ RESULT_KEY_STAMP_MINTED,
93
+ STAMP_DID_NOT_CONVERGE_MESSAGE,
94
+ STAMP_DID_NOT_CONVERGE_RETURNCODE,
95
+ STAMP_STORE_IMPORT_FAILURE_MESSAGE,
96
+ STAMP_STORE_LIVE_SURFACE_HASH_NAME,
97
+ STAMP_STORE_MODULE_FILE_NAME,
98
+ STAMP_STORE_MODULE_NAME,
99
+ STAMP_STORE_RECORD_CLEAN_STAMP_NAME,
100
+ STAMP_STORE_RESOLVE_REPO_ROOT_NAME,
78
101
  SUCCESSFUL_REVIEW_RETURNCODE,
79
102
  )
80
103
  from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
@@ -115,6 +138,25 @@ class CodeReviewOutcome:
115
138
  is_dirty_tree: bool
116
139
 
117
140
 
141
+ @dataclass(frozen=True)
142
+ class StampMintOutcome:
143
+ """Outcome of a ``--record-stamp`` minting run.
144
+
145
+ ::
146
+
147
+ stable rc 0 pass -> is_stamp_minted True, bound_hash set
148
+ cap hit / unstable -> is_stamp_minted False, non-zero returncode
149
+
150
+ The attributes carry the last review, whether a stamp was minted, the pass
151
+ count, and the surface hash the stamp was bound to when minted.
152
+ """
153
+
154
+ review_outcome: CodeReviewOutcome
155
+ is_stamp_minted: bool
156
+ pass_count: int
157
+ bound_hash: str | None
158
+
159
+
118
160
  review_claude_runner = run_claude
119
161
  review_host_profile_detector = detect_host_profile
120
162
  review_git_status_runner = subprocess.run
@@ -125,6 +167,65 @@ TextCapturingSubprocessRunner = Callable[
125
167
  ]
126
168
 
127
169
 
170
+ def _stamp_store_file_path() -> Path:
171
+ """Return the stamp store module path in the sibling hooks/blocking tree."""
172
+ blocking_directory = Path(__file__).resolve().parent.parent / "hooks" / "blocking"
173
+ return blocking_directory / STAMP_STORE_MODULE_FILE_NAME
174
+
175
+
176
+ def _load_store_from_spec(store_file_path: Path) -> ModuleType:
177
+ """Import the stamp store from *store_file_path*, re-raising a missing dep."""
178
+ blocking_directory_string = str(store_file_path.parent)
179
+ if blocking_directory_string not in sys.path:
180
+ sys.path.insert(0, blocking_directory_string)
181
+ module_spec = importlib.util.spec_from_file_location(
182
+ STAMP_STORE_MODULE_NAME, store_file_path
183
+ )
184
+ if module_spec is None or module_spec.loader is None:
185
+ raise ModuleNotFoundError(
186
+ f"could not create import spec for {store_file_path}",
187
+ name=STAMP_STORE_MODULE_NAME,
188
+ )
189
+ store_module = importlib.util.module_from_spec(module_spec)
190
+ sys.modules[STAMP_STORE_MODULE_NAME] = store_module
191
+ try:
192
+ module_spec.loader.exec_module(store_module)
193
+ except ModuleNotFoundError:
194
+ del sys.modules[STAMP_STORE_MODULE_NAME]
195
+ raise
196
+ return store_module
197
+
198
+
199
+ def load_code_review_stamp_store() -> ModuleType:
200
+ """Import the stamp store module from the installed or repo hooks tree.
201
+
202
+ ::
203
+
204
+ code_review_stamp_store.py present -> module with record_clean_stamp
205
+ module file missing -> ModuleNotFoundError (loud)
206
+
207
+ A genuine missing dependency of the store still raises rather than being
208
+ swallowed, so ``--record-stamp`` fails loudly when it cannot mint.
209
+
210
+ Returns:
211
+ The loaded ``code_review_stamp_store`` module.
212
+
213
+ Raises:
214
+ ModuleNotFoundError: When the store file is absent or a real import
215
+ dependency of the store is missing.
216
+ """
217
+ store_file_path = _stamp_store_file_path()
218
+ if not store_file_path.is_file():
219
+ raise ModuleNotFoundError(
220
+ f"code review stamp store not found at {store_file_path}",
221
+ name=STAMP_STORE_MODULE_NAME,
222
+ )
223
+ cached_module = sys.modules.get(STAMP_STORE_MODULE_NAME)
224
+ if cached_module is not None:
225
+ return cached_module
226
+ return _load_store_from_spec(store_file_path)
227
+
228
+
128
229
  def is_opus_session_model(session_model: str) -> bool:
129
230
  """Return True when *session_model* is the opus short alias (any letter case).
130
231
 
@@ -152,8 +253,6 @@ def decide_review_mode(*, host_profile: str, session_model: str) -> str:
152
253
  # ok: "in_session"
153
254
  decide_review_mode(host_profile="Claude", session_model="sonnet")
154
255
  # ok: "chain"
155
- decide_review_mode(host_profile="ThirdParty", session_model="opus")
156
- # ok: "chain"
157
256
 
158
257
  Args:
159
258
  host_profile: Detected host profile (``Claude`` or ``ThirdParty``).
@@ -168,20 +267,42 @@ def decide_review_mode(*, host_profile: str, session_model: str) -> str:
168
267
  return MODE_CHAIN
169
268
 
170
269
 
171
- def build_code_review_arguments() -> list[str]:
270
+ def build_code_review_prompt(effort: str) -> str:
271
+ """Build the single-turn slash-command prompt for the given effort.
272
+
273
+ ::
274
+
275
+ build_code_review_prompt("low") # ok: "/code-review low --fix"
276
+ build_code_review_prompt("xhigh") # ok: "/code-review xhigh --fix"
277
+
278
+ Args:
279
+ effort: A validated effort token from the ascending order tuple.
280
+
281
+ Returns:
282
+ The ``/code-review <effort> --fix`` prompt string.
283
+ """
284
+ return f"{CODE_REVIEW_SLASH_COMMAND} {effort} {CODE_REVIEW_FIX_FLAG}"
285
+
286
+
287
+ def build_code_review_arguments(
288
+ effort: str = DEFAULT_CODE_REVIEW_EFFORT,
289
+ ) -> list[str]:
172
290
  """Return the argv tokens passed to ``run_claude`` for a chain review.
173
291
 
174
292
  ::
175
293
 
176
- build_code_review_arguments()
177
- # ok: ["-p", CODE_REVIEW_PROMPT, "--model", "opus", ...]
294
+ build_code_review_arguments("high")
295
+ # ok: ["-p", "/code-review high --fix", "--model", "opus", ...]
296
+
297
+ Args:
298
+ effort: Effort token embedded in the slash-command prompt.
178
299
 
179
300
  Returns:
180
301
  Ordered claude CLI arguments for the headless opus review slash command.
181
302
  """
182
303
  return [
183
304
  SINGLE_TURN_FLAG,
184
- CODE_REVIEW_PROMPT,
305
+ build_code_review_prompt(effort),
185
306
  MODEL_FLAG,
186
307
  CODE_REVIEW_MODEL_ALIAS,
187
308
  OUTPUT_FORMAT_FLAG,
@@ -191,6 +312,29 @@ def build_code_review_arguments() -> list[str]:
191
312
  ]
192
313
 
193
314
 
315
+ def validate_effort_token(effort: str) -> str | None:
316
+ """Return an error message when *effort* is unknown or unsupported.
317
+
318
+ ::
319
+
320
+ validate_effort_token("low") # ok: None
321
+ validate_effort_token("ultra") # flag: error mentioning ultra
322
+ validate_effort_token("bogus") # flag: error listing allowed tokens
323
+
324
+ Args:
325
+ effort: Caller-supplied effort token.
326
+
327
+ Returns:
328
+ None when the token is allowed; otherwise a human-readable error.
329
+ """
330
+ if effort in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
331
+ return None
332
+ allowed_tokens = EFFORT_TOKEN_LIST_SEPARATOR.join(
333
+ ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
334
+ )
335
+ return INVALID_EFFORT_MESSAGE.format(effort=effort, allowed=allowed_tokens)
336
+
337
+
194
338
  def is_working_tree_dirty(working_directory: Path) -> bool:
195
339
  """Return True when the tree is dirty or ``git status`` fails.
196
340
 
@@ -282,7 +426,9 @@ def _run_claude_with_empty_stdin(
282
426
  ) -> ChainInvocationOutcome:
283
427
  working_directory_path = str(working_directory)
284
428
  with _CHAIN_RUNNER_LOCK:
285
- previous_runner = chain_runner.chain_subprocess_runner
429
+ previous_runner: TextCapturingSubprocessRunner = (
430
+ chain_runner.chain_subprocess_runner
431
+ )
286
432
 
287
433
  def _runner_with_empty_stdin(
288
434
  all_invocation_tokens: Sequence[str],
@@ -296,6 +442,7 @@ def _run_claude_with_empty_stdin(
296
442
  timeout_for_run = float(maybe_timeout)
297
443
  else:
298
444
  timeout_for_run = None
445
+ forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
299
446
  completed_process: subprocess.CompletedProcess[str] = previous_runner(
300
447
  all_invocation_tokens,
301
448
  capture_output=True,
@@ -304,6 +451,7 @@ def _run_claude_with_empty_stdin(
304
451
  check=False,
305
452
  stdin=subprocess.DEVNULL,
306
453
  cwd=working_directory_path,
454
+ **forwarded_text_codec,
307
455
  )
308
456
  return completed_process
309
457
 
@@ -347,24 +495,55 @@ def _failure_code_review_outcome(returncode: int) -> CodeReviewOutcome:
347
495
  is_dirty_tree=False,
348
496
  )
349
497
 
498
+ def _run_chain_review(
499
+ *,
500
+ working_directory: Path,
501
+ timeout_seconds: int,
502
+ effort: str,
503
+ ) -> CodeReviewOutcome:
504
+ all_claude_arguments = build_code_review_arguments(effort)
505
+ chain_outcome = _run_claude_with_empty_stdin(
506
+ all_claude_arguments,
507
+ timeout_seconds=timeout_seconds,
508
+ working_directory=working_directory,
509
+ )
510
+ return _chain_outcome(chain_outcome, working_directory=working_directory)
511
+
512
+
350
513
  def invoke_code_review(
351
514
  *,
352
515
  working_directory: Path,
353
516
  session_model: str,
354
517
  timeout_seconds: int,
518
+ effort: str = DEFAULT_CODE_REVIEW_EFFORT,
519
+ is_force_chain: bool = False,
355
520
  ) -> CodeReviewOutcome:
356
521
  """Run or hand off ``/code-review`` based on host profile and session model.
357
522
 
523
+ ::
524
+
525
+ Claude + opus, is_force_chain False -> in_session (no spawn)
526
+ Claude + opus, is_force_chain True -> chain (headless spawn)
527
+ any host with sonnet -> chain
528
+
358
529
  Args:
359
530
  working_directory: PR working tree used as cwd for the chain spawn.
360
- session_model: Caller-stated session model short alias (for example
361
- ``opus`` or ``sonnet``).
531
+ session_model: Caller-stated session model short alias.
362
532
  timeout_seconds: Timeout applied to each chain binary invocation.
533
+ effort: Effort token embedded in the ``/code-review`` prompt.
534
+ is_force_chain: When True, always spawn chain mode (used by
535
+ ``--record-stamp`` so the invoker observes the review).
363
536
 
364
537
  Returns:
365
538
  Structured outcome including mode, served binary, return code, and
366
539
  whether the working tree is dirty after a chain run.
367
540
  """
541
+ if is_force_chain:
542
+ return _run_chain_review(
543
+ working_directory=working_directory,
544
+ timeout_seconds=timeout_seconds,
545
+ effort=effort,
546
+ )
368
547
  host_profile = review_host_profile_detector()
369
548
  review_mode = decide_review_mode(
370
549
  host_profile=host_profile,
@@ -372,13 +551,193 @@ def invoke_code_review(
372
551
  )
373
552
  if review_mode == MODE_IN_SESSION:
374
553
  return _in_session_outcome()
375
- all_claude_arguments = build_code_review_arguments()
376
- chain_outcome = _run_claude_with_empty_stdin(
377
- all_claude_arguments,
554
+ return _run_chain_review(
555
+ working_directory=working_directory,
378
556
  timeout_seconds=timeout_seconds,
557
+ effort=effort,
558
+ )
559
+
560
+
561
+ def _surface_hash_before_and_after_are_stable(
562
+ before_hash: str | None,
563
+ after_hash: str | None,
564
+ ) -> bool:
565
+ if before_hash is None:
566
+ return False
567
+ if after_hash is None:
568
+ return False
569
+ return before_hash == after_hash
570
+
571
+
572
+ def _mint_stamp_for_stable_pass(
573
+ *,
574
+ store_module: ModuleType,
575
+ working_directory: Path,
576
+ surface_hash: str,
577
+ effort: str,
578
+ ) -> bool:
579
+ resolve_repo_root = getattr(store_module, STAMP_STORE_RESOLVE_REPO_ROOT_NAME)
580
+ record_clean_stamp = getattr(store_module, STAMP_STORE_RECORD_CLEAN_STAMP_NAME)
581
+ repo_root = resolve_repo_root(str(working_directory))
582
+ if repo_root is None:
583
+ return False
584
+ record_clean_stamp(repo_root, surface_hash, effort)
585
+ return True
586
+
587
+
588
+ def _mint_outcome_when_stable_clean(
589
+ *,
590
+ store_module: ModuleType,
591
+ working_directory: Path,
592
+ effort: str,
593
+ pass_number: int,
594
+ before_hash: str,
595
+ review_outcome: CodeReviewOutcome,
596
+ ) -> StampMintOutcome | None:
597
+ is_minted = _mint_stamp_for_stable_pass(
598
+ store_module=store_module,
379
599
  working_directory=working_directory,
600
+ surface_hash=before_hash,
601
+ effort=effort,
602
+ )
603
+ if not is_minted:
604
+ return None
605
+ return StampMintOutcome(
606
+ review_outcome=review_outcome,
607
+ is_stamp_minted=True,
608
+ pass_count=pass_number,
609
+ bound_hash=before_hash,
610
+ )
611
+
612
+
613
+ def _unminted_pass_outcome(
614
+ review_outcome: CodeReviewOutcome, pass_number: int
615
+ ) -> StampMintOutcome:
616
+ return StampMintOutcome(
617
+ review_outcome=review_outcome,
618
+ is_stamp_minted=False,
619
+ pass_count=pass_number,
620
+ bound_hash=None,
621
+ )
622
+
623
+
624
+ def _stamp_outcome_for_pass(
625
+ *,
626
+ store_module: ModuleType,
627
+ working_directory: Path,
628
+ effort: str,
629
+ pass_number: int,
630
+ before_hash: str | None,
631
+ after_hash: str | None,
632
+ review_outcome: CodeReviewOutcome,
633
+ ) -> StampMintOutcome | None:
634
+ is_stable = _surface_hash_before_and_after_are_stable(before_hash, after_hash)
635
+ is_successful = is_successful_code_review(review_outcome)
636
+ is_empty_surface = before_hash is None and after_hash is None
637
+ if is_successful and is_empty_surface:
638
+ return _unminted_pass_outcome(review_outcome, pass_number)
639
+ if is_successful and is_stable and before_hash is not None:
640
+ minted = _mint_outcome_when_stable_clean(
641
+ store_module=store_module,
642
+ working_directory=working_directory,
643
+ effort=effort,
644
+ pass_number=pass_number,
645
+ before_hash=before_hash,
646
+ review_outcome=review_outcome,
647
+ )
648
+ if minted is not None:
649
+ return minted
650
+ if is_stable:
651
+ return _unminted_pass_outcome(review_outcome, pass_number)
652
+ return None
653
+
654
+
655
+ def _run_one_stamp_mint_pass(
656
+ *,
657
+ store_module: ModuleType,
658
+ live_surface_hash: Callable[..., str | None],
659
+ working_directory: Path,
660
+ timeout_seconds: int,
661
+ effort: str,
662
+ pass_number: int,
663
+ ) -> tuple[CodeReviewOutcome, StampMintOutcome | None]:
664
+ before_hash = live_surface_hash(str(working_directory))
665
+ review_outcome = invoke_code_review(
666
+ working_directory=working_directory,
667
+ session_model=CODE_REVIEW_MODEL_ALIAS,
668
+ timeout_seconds=timeout_seconds,
669
+ effort=effort,
670
+ is_force_chain=True,
671
+ )
672
+ after_hash = live_surface_hash(str(working_directory))
673
+ maybe_outcome = _stamp_outcome_for_pass(
674
+ store_module=store_module,
675
+ working_directory=working_directory,
676
+ effort=effort,
677
+ pass_number=pass_number,
678
+ before_hash=before_hash,
679
+ after_hash=after_hash,
680
+ review_outcome=review_outcome,
681
+ )
682
+ return review_outcome, maybe_outcome
683
+
684
+
685
+ def _iterate_stamp_mint_passes(
686
+ *,
687
+ store_module: ModuleType,
688
+ live_surface_hash: Callable[..., str | None],
689
+ working_directory: Path,
690
+ timeout_seconds: int,
691
+ effort: str,
692
+ maximum_passes: int,
693
+ ) -> StampMintOutcome:
694
+ last_review_outcome = _failure_code_review_outcome(
695
+ STAMP_DID_NOT_CONVERGE_RETURNCODE
696
+ )
697
+ for each_pass_number in range(1, maximum_passes + 1):
698
+ last_review_outcome, maybe_mint_outcome = _run_one_stamp_mint_pass(
699
+ store_module=store_module,
700
+ live_surface_hash=live_surface_hash,
701
+ working_directory=working_directory,
702
+ timeout_seconds=timeout_seconds,
703
+ effort=effort,
704
+ pass_number=each_pass_number,
705
+ )
706
+ if maybe_mint_outcome is not None:
707
+ return maybe_mint_outcome
708
+ return _unminted_pass_outcome(last_review_outcome, maximum_passes)
709
+
710
+
711
+ def invoke_code_review_and_record_stamp(
712
+ *,
713
+ working_directory: Path,
714
+ session_model: str,
715
+ timeout_seconds: int,
716
+ effort: str,
717
+ maximum_passes: int = MAXIMUM_STAMP_MINT_PASSES,
718
+ ) -> StampMintOutcome:
719
+ """Force chain review passes until the surface is stable, then mint a stamp.
720
+
721
+ Args:
722
+ working_directory: PR working tree used as cwd for each chain spawn.
723
+ session_model: Session model short alias (unused; chain mode forced).
724
+ timeout_seconds: Timeout applied to each chain binary invocation.
725
+ effort: Effort token the clean review records on the stamp.
726
+ maximum_passes: Hard cap on review passes before giving up.
727
+
728
+ Returns:
729
+ A StampMintOutcome for the last review and whether a stamp was minted.
730
+ """
731
+ del session_model
732
+ store_module = load_code_review_stamp_store()
733
+ return _iterate_stamp_mint_passes(
734
+ store_module=store_module,
735
+ live_surface_hash=getattr(store_module, STAMP_STORE_LIVE_SURFACE_HASH_NAME),
736
+ working_directory=working_directory,
737
+ timeout_seconds=timeout_seconds,
738
+ effort=effort,
739
+ maximum_passes=maximum_passes,
380
740
  )
381
- return _chain_outcome(chain_outcome, working_directory=working_directory)
382
741
 
383
742
 
384
743
  def encode_code_review_outcome(
@@ -400,12 +759,28 @@ def encode_code_review_outcome(
400
759
  }
401
760
 
402
761
 
403
- def _build_argument_parser() -> argparse.ArgumentParser:
404
- parser = argparse.ArgumentParser(
405
- description=(
406
- "Run built-in /code-review at high effort on opus, host-aware."
407
- )
408
- )
762
+ def encode_stamp_mint_outcome(
763
+ mint_outcome: StampMintOutcome,
764
+ ) -> dict[str, object]:
765
+ """Encode a stamp-mint outcome as the JSON-serializable payload.
766
+
767
+ Args:
768
+ mint_outcome: The ``--record-stamp`` outcome to encode.
769
+
770
+ Returns:
771
+ A plain dict with the review fields plus mint metadata.
772
+ """
773
+ encoded_payload = encode_code_review_outcome(mint_outcome.review_outcome)
774
+ encoded_payload[RESULT_KEY_STAMP_MINTED] = mint_outcome.is_stamp_minted
775
+ encoded_payload[RESULT_KEY_PASS_COUNT] = mint_outcome.pass_count
776
+ encoded_payload[RESULT_KEY_BOUND_HASH] = mint_outcome.bound_hash
777
+ if not mint_outcome.is_stamp_minted:
778
+ if mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES:
779
+ encoded_payload[RESULT_KEY_RETURNCODE] = STAMP_DID_NOT_CONVERGE_RETURNCODE
780
+ return encoded_payload
781
+
782
+
783
+ def _add_review_arguments(parser: argparse.ArgumentParser) -> None:
409
784
  parser.add_argument(
410
785
  CWD_FLAG,
411
786
  dest="working_directory",
@@ -426,37 +801,174 @@ def _build_argument_parser() -> argparse.ArgumentParser:
426
801
  default=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
427
802
  help="Timeout in seconds applied to each chain binary invocation.",
428
803
  )
804
+
805
+
806
+ def _add_stamp_and_effort_arguments(parser: argparse.ArgumentParser) -> None:
807
+ parser.add_argument(
808
+ RECORD_STAMP_FLAG,
809
+ dest="is_record_stamp",
810
+ action="store_true",
811
+ help=CLI_RECORD_STAMP_HELP,
812
+ )
813
+ parser.add_argument(
814
+ CLI_EFFORT_METAVAR,
815
+ nargs="?",
816
+ default=DEFAULT_CODE_REVIEW_EFFORT,
817
+ help=CLI_EFFORT_HELP,
818
+ )
819
+
820
+
821
+ def _build_argument_parser() -> argparse.ArgumentParser:
822
+ parser = argparse.ArgumentParser(
823
+ description=(
824
+ "Run built-in /code-review at a chosen effort on opus, host-aware."
825
+ )
826
+ )
827
+ _add_review_arguments(parser)
828
+ _add_stamp_and_effort_arguments(parser)
429
829
  return parser
430
830
 
431
831
 
832
+ def _emit_invalid_effort_and_exit_code(effort: str) -> int:
833
+ error_message = validate_effort_token(effort)
834
+ if error_message is None:
835
+ return SUCCESSFUL_REVIEW_RETURNCODE
836
+ sys.stderr.write(error_message + "\n")
837
+ return INVALID_EFFORT_RETURNCODE
838
+
839
+
840
+ def _import_failure_payload() -> dict[str, object]:
841
+ failure_outcome = _failure_code_review_outcome(INVALID_EFFORT_RETURNCODE)
842
+ encoded_payload = encode_code_review_outcome(failure_outcome)
843
+ encoded_payload[RESULT_KEY_STAMP_MINTED] = False
844
+ encoded_payload[RESULT_KEY_PASS_COUNT] = 0
845
+ encoded_payload[RESULT_KEY_BOUND_HASH] = None
846
+ return encoded_payload
847
+
848
+
849
+ def _no_mint_outcome(returncode: int) -> StampMintOutcome:
850
+ return StampMintOutcome(
851
+ review_outcome=_failure_code_review_outcome(returncode),
852
+ is_stamp_minted=False,
853
+ pass_count=0,
854
+ bound_hash=None,
855
+ )
856
+
857
+
858
+ def _mint_or_config_outcome(
859
+ *,
860
+ working_directory: Path,
861
+ session_model: str,
862
+ timeout_seconds: int,
863
+ effort: str,
864
+ ) -> StampMintOutcome:
865
+ try:
866
+ return invoke_code_review_and_record_stamp(
867
+ working_directory=working_directory,
868
+ session_model=session_model,
869
+ timeout_seconds=timeout_seconds,
870
+ effort=effort,
871
+ )
872
+ except ChainConfigurationError:
873
+ return _no_mint_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
874
+ except ValueError:
875
+ return _no_mint_outcome(HOST_PROFILE_ERROR_RETURNCODE)
876
+
877
+
878
+ def _emit_import_failure(import_error: ModuleNotFoundError) -> int:
879
+ sys.stderr.write(
880
+ STAMP_STORE_IMPORT_FAILURE_MESSAGE.format(error=import_error) + "\n"
881
+ )
882
+ sys.stdout.write(json.dumps(_import_failure_payload()) + "\n")
883
+ return INVALID_EFFORT_RETURNCODE
884
+
885
+
886
+ def _record_stamp_exit_code(mint_outcome: StampMintOutcome) -> int:
887
+ if mint_outcome.is_stamp_minted:
888
+ return mint_outcome.review_outcome.returncode
889
+ if mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES:
890
+ return STAMP_DID_NOT_CONVERGE_RETURNCODE
891
+ return mint_outcome.review_outcome.returncode
892
+
893
+
894
+ def _emit_mint_outcome(mint_outcome: StampMintOutcome) -> int:
895
+ encoded_payload = encode_stamp_mint_outcome(mint_outcome)
896
+ did_not_converge = (
897
+ not mint_outcome.is_stamp_minted
898
+ and mint_outcome.pass_count >= MAXIMUM_STAMP_MINT_PASSES
899
+ )
900
+ if did_not_converge:
901
+ sys.stderr.write(
902
+ STAMP_DID_NOT_CONVERGE_MESSAGE.format(pass_count=mint_outcome.pass_count)
903
+ + "\n"
904
+ )
905
+ sys.stdout.write(json.dumps(encoded_payload) + "\n")
906
+ return _record_stamp_exit_code(mint_outcome)
907
+
908
+
909
+ def _run_record_stamp_cli(
910
+ *,
911
+ working_directory: Path,
912
+ session_model: str,
913
+ timeout_seconds: int,
914
+ effort: str,
915
+ ) -> int:
916
+ try:
917
+ mint_outcome = _mint_or_config_outcome(
918
+ working_directory=working_directory,
919
+ session_model=session_model,
920
+ timeout_seconds=timeout_seconds,
921
+ effort=effort,
922
+ )
923
+ except ModuleNotFoundError as import_error:
924
+ return _emit_import_failure(import_error)
925
+ return _emit_mint_outcome(mint_outcome)
926
+
927
+
928
+ def _run_plain_review_cli(*, parsed_arguments: argparse.Namespace, effort: str) -> int:
929
+ try:
930
+ review_outcome = invoke_code_review(
931
+ working_directory=parsed_arguments.working_directory,
932
+ session_model=parsed_arguments.session_model,
933
+ timeout_seconds=parsed_arguments.timeout_seconds,
934
+ effort=effort,
935
+ )
936
+ except ChainConfigurationError:
937
+ review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
938
+ except ValueError:
939
+ review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
940
+ sys.stdout.write(json.dumps(encode_code_review_outcome(review_outcome)) + "\n")
941
+ return review_outcome.returncode
942
+
943
+
432
944
  def main(all_command_arguments: list[str]) -> int:
433
945
  """Run the invoker for CLI arguments and print the JSON outcome.
434
946
 
435
- ``ChainConfigurationError`` and host ``ValueError`` still emit result JSON
436
- on stdout (no traceback-only failure). The non-zero return code and null
437
- ``served_command`` block a clean stamp.
947
+ ``--record-stamp`` forces chain mode and mints only on a surface-stable
948
+ returncode-0 pass; an unknown or ``ultra`` effort exits non-zero before any
949
+ review runs.
438
950
 
439
951
  Args:
440
952
  all_command_arguments: The argument vector after the program name.
441
953
 
442
954
  Returns:
443
- The outcome return code (``0`` for in-session; chain return code otherwise).
955
+ The outcome return code (``0`` for in-session; chain return code
956
+ otherwise; non-zero when effort is invalid or minting fails to converge).
444
957
  """
445
958
  parser = _build_argument_parser()
446
959
  parsed_arguments = parser.parse_args(all_command_arguments)
447
- try:
448
- review_outcome = invoke_code_review(
960
+ effort_token = str(parsed_arguments.effort)
961
+ invalid_effort_exit_code = _emit_invalid_effort_and_exit_code(effort_token)
962
+ if invalid_effort_exit_code != SUCCESSFUL_REVIEW_RETURNCODE:
963
+ return invalid_effort_exit_code
964
+ if parsed_arguments.is_record_stamp:
965
+ return _run_record_stamp_cli(
449
966
  working_directory=parsed_arguments.working_directory,
450
967
  session_model=parsed_arguments.session_model,
451
968
  timeout_seconds=parsed_arguments.timeout_seconds,
969
+ effort=effort_token,
452
970
  )
453
- except ChainConfigurationError:
454
- review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
455
- except ValueError:
456
- review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
457
- encoded_payload = encode_code_review_outcome(review_outcome)
458
- sys.stdout.write(json.dumps(encoded_payload) + "\n")
459
- return review_outcome.returncode
971
+ return _run_plain_review_cli(parsed_arguments=parsed_arguments, effort=effort_token)
460
972
 
461
973
 
462
974
  if __name__ == "__main__":