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.
- package/CLAUDE.md +3 -2
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +3 -3
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +13 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -903
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +27 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +2 -2
|
@@ -50,6 +50,9 @@ from claude_chain_runner import ( # noqa: E402
|
|
|
50
50
|
ChainInvocationOutcome,
|
|
51
51
|
run_claude,
|
|
52
52
|
)
|
|
53
|
+
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
54
|
+
collect_forwarded_text_codec,
|
|
55
|
+
)
|
|
53
56
|
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
54
57
|
AGENT_FLAG,
|
|
55
58
|
ALL_AGENT_FILENAMES_BY_ROLE,
|
|
@@ -177,7 +180,9 @@ def _run_claude_with_headless_overrides(
|
|
|
177
180
|
) -> ChainInvocationOutcome:
|
|
178
181
|
working_directory_path = str(working_directory)
|
|
179
182
|
with _HEADLESS_CHAIN_RUNNER_LOCK:
|
|
180
|
-
previous_runner =
|
|
183
|
+
previous_runner: TextCapturingSubprocessRunner = (
|
|
184
|
+
chain_runner.chain_subprocess_runner
|
|
185
|
+
)
|
|
181
186
|
|
|
182
187
|
def _runner_with_headless_overrides(
|
|
183
188
|
all_invocation_tokens: Sequence[str],
|
|
@@ -186,6 +191,7 @@ def _run_claude_with_headless_overrides(
|
|
|
186
191
|
) -> subprocess.CompletedProcess[str]:
|
|
187
192
|
del all_positionals
|
|
188
193
|
prompt_stdin.seek(0)
|
|
194
|
+
forwarded_text_codec = collect_forwarded_text_codec(all_keywords)
|
|
189
195
|
completed_process: subprocess.CompletedProcess[str] = previous_runner(
|
|
190
196
|
all_invocation_tokens,
|
|
191
197
|
capture_output=True,
|
|
@@ -194,6 +200,7 @@ def _run_claude_with_headless_overrides(
|
|
|
194
200
|
check=False,
|
|
195
201
|
stdin=prompt_stdin,
|
|
196
202
|
cwd=working_directory_path,
|
|
203
|
+
**forwarded_text_codec,
|
|
197
204
|
)
|
|
198
205
|
return completed_process
|
|
199
206
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Behavioral tests for the host-aware ``/code-review`` invoker."""
|
|
1
|
+
"""Behavioral tests for the host-aware ``/code-review`` invoker and stamps."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -28,7 +28,7 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
|
28
28
|
from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
29
29
|
CLI_SESSION_MODEL_FLAG,
|
|
30
30
|
CODE_REVIEW_MODEL_ALIAS,
|
|
31
|
-
|
|
31
|
+
DEFAULT_CODE_REVIEW_EFFORT,
|
|
32
32
|
GIT_BINARY,
|
|
33
33
|
GIT_PORCELAIN_FLAG,
|
|
34
34
|
GIT_STATUS_SUBCOMMAND,
|
|
@@ -296,7 +296,7 @@ def test_chain_argv_assembly(
|
|
|
296
296
|
|
|
297
297
|
assert call_log.claude_arguments == [
|
|
298
298
|
SINGLE_TURN_FLAG,
|
|
299
|
-
|
|
299
|
+
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
300
300
|
MODEL_FLAG,
|
|
301
301
|
CODE_REVIEW_MODEL_ALIAS,
|
|
302
302
|
OUTPUT_FORMAT_FLAG,
|
|
@@ -412,7 +412,7 @@ def test_build_code_review_arguments_matches_contract() -> None:
|
|
|
412
412
|
all_arguments = invoker.build_code_review_arguments()
|
|
413
413
|
assert all_arguments == [
|
|
414
414
|
SINGLE_TURN_FLAG,
|
|
415
|
-
|
|
415
|
+
invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
|
|
416
416
|
MODEL_FLAG,
|
|
417
417
|
CODE_REVIEW_MODEL_ALIAS,
|
|
418
418
|
OUTPUT_FORMAT_FLAG,
|
|
@@ -670,3 +670,297 @@ def test_encode_code_review_outcome_shape() -> None:
|
|
|
670
670
|
RESULT_KEY_RETURNCODE: FIXTURE_CHAIN_RETURNCODE,
|
|
671
671
|
RESULT_KEY_DIRTY_TREE: True,
|
|
672
672
|
}
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
EFFORT_LOW = "low"
|
|
676
|
+
REJECTED_ULTRA_EFFORT = "ultra"
|
|
677
|
+
RECORD_STAMP_MINT_CAP = 3
|
|
678
|
+
SINGLE_PASS_CAP = 1
|
|
679
|
+
INVALID_EFFORT_EXIT_CODE = 2
|
|
680
|
+
DID_NOT_CONVERGE_EXIT_CODE = 1
|
|
681
|
+
RECORD_STAMP_CLI_FLAG = "--record-stamp"
|
|
682
|
+
RESULT_STAMP_MINTED_KEY = "stamp_minted"
|
|
683
|
+
RESULT_PASS_COUNT_KEY = "pass_count"
|
|
684
|
+
RESULT_BOUND_HASH_KEY = "bound_hash"
|
|
685
|
+
MISSING_STORE_FILE_NAME = "code_review_stamp_store_absent.py"
|
|
686
|
+
SURFACE_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
687
|
+
SURFACE_CHANGE_SOURCE = "def add(left: int, right: int) -> int:\n return left - right\n"
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _run_git(repository_directory: Path, *git_arguments: str) -> None:
|
|
691
|
+
subprocess.run(
|
|
692
|
+
[GIT_BINARY, "-C", str(repository_directory), *git_arguments],
|
|
693
|
+
check=True,
|
|
694
|
+
capture_output=True,
|
|
695
|
+
text=True,
|
|
696
|
+
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _make_repo_with_change_surface(tmp_path: Path) -> Path:
|
|
701
|
+
origin_directory = tmp_path / "origin.git"
|
|
702
|
+
work_directory = tmp_path / "work"
|
|
703
|
+
work_directory.mkdir()
|
|
704
|
+
subprocess.run(
|
|
705
|
+
[GIT_BINARY, "init", "--bare", "--initial-branch=main", str(origin_directory)],
|
|
706
|
+
check=True,
|
|
707
|
+
capture_output=True,
|
|
708
|
+
text=True,
|
|
709
|
+
timeout=GIT_INIT_TIMEOUT_SECONDS,
|
|
710
|
+
)
|
|
711
|
+
_run_git(work_directory, "init", "--initial-branch=main")
|
|
712
|
+
_run_git(work_directory, "config", "user.email", "tests@example.com")
|
|
713
|
+
_run_git(work_directory, "config", "user.name", "Reviewer")
|
|
714
|
+
(work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
|
|
715
|
+
_run_git(work_directory, "add", "-A")
|
|
716
|
+
_run_git(work_directory, "commit", "-m", "base")
|
|
717
|
+
_run_git(work_directory, "remote", "add", "origin", str(origin_directory))
|
|
718
|
+
_run_git(work_directory, "push", "-u", "origin", "main")
|
|
719
|
+
(work_directory / "app.py").write_text(SURFACE_CHANGE_SOURCE, encoding="utf-8")
|
|
720
|
+
return work_directory
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
|
|
724
|
+
home_text = str(fake_home)
|
|
725
|
+
monkeypatch.setenv("HOME", home_text)
|
|
726
|
+
monkeypatch.setenv("USERPROFILE", home_text)
|
|
727
|
+
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
728
|
+
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _prepared_surface_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
|
732
|
+
fake_home = tmp_path / "home"
|
|
733
|
+
fake_home.mkdir()
|
|
734
|
+
_isolate_home(monkeypatch, fake_home)
|
|
735
|
+
return _make_repo_with_change_surface(tmp_path)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _chain_clean_outcome() -> invoker.CodeReviewOutcome:
|
|
739
|
+
return invoker.CodeReviewOutcome(
|
|
740
|
+
mode=MODE_CHAIN,
|
|
741
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
742
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
743
|
+
is_dirty_tree=True,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _stable_clean_review(**_review_keywords: object) -> invoker.CodeReviewOutcome:
|
|
748
|
+
return _chain_clean_outcome()
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _surface_changing_review(
|
|
752
|
+
*, working_directory: Path, **_review_keywords: object
|
|
753
|
+
) -> invoker.CodeReviewOutcome:
|
|
754
|
+
applied_fix_path = working_directory / DIRTY_FILE_NAME
|
|
755
|
+
applied_fix_path.write_text(DIRTY_FILE_CONTENTS, encoding="utf-8")
|
|
756
|
+
return _chain_clean_outcome()
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class _DriftingReview:
|
|
760
|
+
def __init__(self) -> None:
|
|
761
|
+
self.pass_count = 0
|
|
762
|
+
|
|
763
|
+
def __call__(
|
|
764
|
+
self, *, working_directory: Path, **_review_keywords: object
|
|
765
|
+
) -> invoker.CodeReviewOutcome:
|
|
766
|
+
self.pass_count += 1
|
|
767
|
+
drift_path = working_directory / f"fix_{self.pass_count}.txt"
|
|
768
|
+
drift_path.write_text(str(self.pass_count), encoding="utf-8")
|
|
769
|
+
return _chain_clean_outcome()
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
@pytest.mark.parametrize("valid_effort", ["low", "medium", "high", "xhigh", "max"])
|
|
773
|
+
def test_validate_effort_token_accepts_known_tokens(valid_effort: str) -> None:
|
|
774
|
+
assert invoker.validate_effort_token(valid_effort) is None
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_validate_effort_token_rejects_ultra_loudly() -> None:
|
|
778
|
+
error_message = invoker.validate_effort_token(REJECTED_ULTRA_EFFORT)
|
|
779
|
+
assert error_message is not None
|
|
780
|
+
assert REJECTED_ULTRA_EFFORT in error_message
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def test_validate_effort_token_rejects_unknown_token() -> None:
|
|
784
|
+
error_message = invoker.validate_effort_token("bogus")
|
|
785
|
+
assert error_message is not None
|
|
786
|
+
assert "bogus" in error_message
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_build_code_review_prompt_reads_as_slash_command() -> None:
|
|
790
|
+
assert invoker.build_code_review_prompt(EFFORT_LOW) == "/code-review low --fix"
|
|
791
|
+
assert invoker.build_code_review_prompt("xhigh") == "/code-review xhigh --fix"
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def test_cli_rejects_ultra_effort_with_nonzero_exit(
|
|
795
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
796
|
+
tmp_path: Path,
|
|
797
|
+
capsys: pytest.CaptureFixture[str],
|
|
798
|
+
) -> None:
|
|
799
|
+
working_directory = _init_git_repository(tmp_path / "repo")
|
|
800
|
+
_install_seams(
|
|
801
|
+
monkeypatch,
|
|
802
|
+
host_profile=HOST_PROFILE_CLAUDE,
|
|
803
|
+
claude_outcome=None,
|
|
804
|
+
working_directory=working_directory,
|
|
805
|
+
)
|
|
806
|
+
exit_code = invoker.main(
|
|
807
|
+
[
|
|
808
|
+
CWD_FLAG,
|
|
809
|
+
str(working_directory),
|
|
810
|
+
CLI_SESSION_MODEL_FLAG,
|
|
811
|
+
FIXTURE_SESSION_OPUS,
|
|
812
|
+
REJECTED_ULTRA_EFFORT,
|
|
813
|
+
]
|
|
814
|
+
)
|
|
815
|
+
assert exit_code == INVALID_EFFORT_EXIT_CODE
|
|
816
|
+
assert REJECTED_ULTRA_EFFORT in capsys.readouterr().err
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def test_record_stamp_mints_on_surface_stable_clean_pass(
|
|
820
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
821
|
+
) -> None:
|
|
822
|
+
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
823
|
+
monkeypatch.setattr(invoker, "invoke_code_review", _stable_clean_review)
|
|
824
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
825
|
+
working_directory=working_directory,
|
|
826
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
827
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
828
|
+
effort=EFFORT_LOW,
|
|
829
|
+
)
|
|
830
|
+
assert mint_outcome.is_stamp_minted is True
|
|
831
|
+
assert mint_outcome.bound_hash is not None
|
|
832
|
+
store_module = invoker.load_code_review_stamp_store()
|
|
833
|
+
assert store_module.stamp_covers_surface(
|
|
834
|
+
str(working_directory), mint_outcome.bound_hash, EFFORT_LOW
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def test_record_stamp_does_not_mint_when_review_changes_surface(
|
|
839
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
840
|
+
) -> None:
|
|
841
|
+
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
842
|
+
monkeypatch.setattr(invoker, "invoke_code_review", _surface_changing_review)
|
|
843
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
844
|
+
working_directory=working_directory,
|
|
845
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
846
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
847
|
+
effort=EFFORT_LOW,
|
|
848
|
+
maximum_passes=SINGLE_PASS_CAP,
|
|
849
|
+
)
|
|
850
|
+
assert mint_outcome.is_stamp_minted is False
|
|
851
|
+
assert mint_outcome.pass_count == SINGLE_PASS_CAP
|
|
852
|
+
assert mint_outcome.bound_hash is None
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def test_record_stamp_hits_cap_without_minting(
|
|
856
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
857
|
+
) -> None:
|
|
858
|
+
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
859
|
+
monkeypatch.setattr(invoker, "invoke_code_review", _DriftingReview())
|
|
860
|
+
mint_outcome = invoker.invoke_code_review_and_record_stamp(
|
|
861
|
+
working_directory=working_directory,
|
|
862
|
+
session_model=CODE_REVIEW_MODEL_ALIAS,
|
|
863
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
864
|
+
effort=EFFORT_LOW,
|
|
865
|
+
maximum_passes=RECORD_STAMP_MINT_CAP,
|
|
866
|
+
)
|
|
867
|
+
assert mint_outcome.is_stamp_minted is False
|
|
868
|
+
assert mint_outcome.pass_count == RECORD_STAMP_MINT_CAP
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def test_cli_record_stamp_returns_non_convergence_code_on_cap(
|
|
872
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
873
|
+
tmp_path: Path,
|
|
874
|
+
capsys: pytest.CaptureFixture[str],
|
|
875
|
+
) -> None:
|
|
876
|
+
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
877
|
+
monkeypatch.setattr(invoker, "invoke_code_review", _DriftingReview())
|
|
878
|
+
exit_code = invoker.main(
|
|
879
|
+
[
|
|
880
|
+
CWD_FLAG,
|
|
881
|
+
str(working_directory),
|
|
882
|
+
CLI_SESSION_MODEL_FLAG,
|
|
883
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
884
|
+
RECORD_STAMP_CLI_FLAG,
|
|
885
|
+
EFFORT_LOW,
|
|
886
|
+
]
|
|
887
|
+
)
|
|
888
|
+
assert exit_code == DID_NOT_CONVERGE_EXIT_CODE
|
|
889
|
+
parsed_payload = json.loads(capsys.readouterr().out)
|
|
890
|
+
assert parsed_payload[RESULT_STAMP_MINTED_KEY] is False
|
|
891
|
+
assert parsed_payload[RESULT_PASS_COUNT_KEY] == RECORD_STAMP_MINT_CAP
|
|
892
|
+
assert parsed_payload[RESULT_BOUND_HASH_KEY] is None
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def test_load_code_review_stamp_store_records_and_covers_surface(
|
|
896
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
897
|
+
) -> None:
|
|
898
|
+
working_directory = _prepared_surface_repo(monkeypatch, tmp_path)
|
|
899
|
+
store_module = invoker.load_code_review_stamp_store()
|
|
900
|
+
surface_hash = store_module.live_surface_hash(str(working_directory))
|
|
901
|
+
assert surface_hash is not None
|
|
902
|
+
stamp_path = store_module.record_clean_stamp(
|
|
903
|
+
str(working_directory), surface_hash, EFFORT_LOW
|
|
904
|
+
)
|
|
905
|
+
assert stamp_path.exists()
|
|
906
|
+
assert store_module.stamp_covers_surface(
|
|
907
|
+
str(working_directory), surface_hash, EFFORT_LOW
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def test_load_code_review_stamp_store_raises_when_file_absent(
|
|
912
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
913
|
+
) -> None:
|
|
914
|
+
monkeypatch.setattr(
|
|
915
|
+
invoker, "STAMP_STORE_MODULE_FILE_NAME", MISSING_STORE_FILE_NAME
|
|
916
|
+
)
|
|
917
|
+
with pytest.raises(ModuleNotFoundError):
|
|
918
|
+
invoker.load_code_review_stamp_store()
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def test_cli_record_stamp_reports_missing_store_dependency(
|
|
922
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
923
|
+
tmp_path: Path,
|
|
924
|
+
capsys: pytest.CaptureFixture[str],
|
|
925
|
+
) -> None:
|
|
926
|
+
working_directory = _init_git_repository(tmp_path / "repo")
|
|
927
|
+
|
|
928
|
+
def raise_missing_store(*all_args: object, **all_keywords: object) -> object:
|
|
929
|
+
del all_args, all_keywords
|
|
930
|
+
raise ModuleNotFoundError("store missing", name="code_review_stamp_store")
|
|
931
|
+
|
|
932
|
+
monkeypatch.setattr(invoker, "load_code_review_stamp_store", raise_missing_store)
|
|
933
|
+
exit_code = invoker.main(
|
|
934
|
+
[
|
|
935
|
+
CWD_FLAG,
|
|
936
|
+
str(working_directory),
|
|
937
|
+
CLI_SESSION_MODEL_FLAG,
|
|
938
|
+
CODE_REVIEW_MODEL_ALIAS,
|
|
939
|
+
RECORD_STAMP_CLI_FLAG,
|
|
940
|
+
EFFORT_LOW,
|
|
941
|
+
]
|
|
942
|
+
)
|
|
943
|
+
assert exit_code == INVALID_EFFORT_EXIT_CODE
|
|
944
|
+
captured = capsys.readouterr()
|
|
945
|
+
assert "stamp store" in captured.err
|
|
946
|
+
parsed_payload = json.loads(captured.out)
|
|
947
|
+
assert parsed_payload[RESULT_STAMP_MINTED_KEY] is False
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def test_encode_stamp_mint_outcome_includes_mint_metadata() -> None:
|
|
951
|
+
review_outcome = invoker.CodeReviewOutcome(
|
|
952
|
+
mode=MODE_CHAIN,
|
|
953
|
+
served_command=FIXTURE_SERVED_COMMAND,
|
|
954
|
+
returncode=FIXTURE_CHAIN_RETURNCODE,
|
|
955
|
+
is_dirty_tree=False,
|
|
956
|
+
)
|
|
957
|
+
mint_outcome = invoker.StampMintOutcome(
|
|
958
|
+
review_outcome=review_outcome,
|
|
959
|
+
is_stamp_minted=True,
|
|
960
|
+
pass_count=SINGLE_PASS_CAP,
|
|
961
|
+
bound_hash="abc123",
|
|
962
|
+
)
|
|
963
|
+
encoded_payload = invoker.encode_stamp_mint_outcome(mint_outcome)
|
|
964
|
+
assert encoded_payload[RESULT_STAMP_MINTED_KEY] is True
|
|
965
|
+
assert encoded_payload[RESULT_PASS_COUNT_KEY] == SINGLE_PASS_CAP
|
|
966
|
+
assert encoded_payload[RESULT_BOUND_HASH_KEY] == "abc123"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Behavioral tests for text codec keyword forwarding in the ``/code-review`` invoker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import invoke_code_review as invoker
|
|
8
|
+
import pytest
|
|
9
|
+
from codec_forwarding_test_support import (
|
|
10
|
+
FIXTURE_CHAIN_ENCODING,
|
|
11
|
+
FIXTURE_CHAIN_ERRORS,
|
|
12
|
+
FIXTURE_ENCODING_KEYWORD_NAME,
|
|
13
|
+
FIXTURE_ERRORS_KEYWORD_NAME,
|
|
14
|
+
install_codec_seams,
|
|
15
|
+
)
|
|
16
|
+
from dev_env_scripts_constants.timing import DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS
|
|
17
|
+
|
|
18
|
+
FIXTURE_CHAIN_STDOUT = '{"result":"review done"}'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _install_invoker_codec_seams(
|
|
22
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
23
|
+
*,
|
|
24
|
+
all_chain_codec_keywords: dict[str, str],
|
|
25
|
+
) -> dict[str, object]:
|
|
26
|
+
return install_codec_seams(
|
|
27
|
+
monkeypatch,
|
|
28
|
+
all_chain_codec_keywords=all_chain_codec_keywords,
|
|
29
|
+
chain_stdout=FIXTURE_CHAIN_STDOUT,
|
|
30
|
+
runner_host=invoker,
|
|
31
|
+
runner_attribute_name="review_claude_runner",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_forwards_text_codec_keywords_to_subprocess_runner(
|
|
36
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
37
|
+
) -> None:
|
|
38
|
+
all_observed_runner_keywords = _install_invoker_codec_seams(
|
|
39
|
+
monkeypatch,
|
|
40
|
+
all_chain_codec_keywords={
|
|
41
|
+
FIXTURE_ENCODING_KEYWORD_NAME: FIXTURE_CHAIN_ENCODING,
|
|
42
|
+
FIXTURE_ERRORS_KEYWORD_NAME: FIXTURE_CHAIN_ERRORS,
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
invoker._run_claude_with_empty_stdin(
|
|
47
|
+
[],
|
|
48
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
49
|
+
working_directory=tmp_path,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert (
|
|
53
|
+
all_observed_runner_keywords[FIXTURE_ENCODING_KEYWORD_NAME]
|
|
54
|
+
== FIXTURE_CHAIN_ENCODING
|
|
55
|
+
)
|
|
56
|
+
assert (
|
|
57
|
+
all_observed_runner_keywords[FIXTURE_ERRORS_KEYWORD_NAME]
|
|
58
|
+
== FIXTURE_CHAIN_ERRORS
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_absent_text_codec_keywords_are_not_invented(
|
|
63
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
64
|
+
) -> None:
|
|
65
|
+
all_observed_runner_keywords = _install_invoker_codec_seams(
|
|
66
|
+
monkeypatch,
|
|
67
|
+
all_chain_codec_keywords={},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
invoker._run_claude_with_empty_stdin(
|
|
71
|
+
[],
|
|
72
|
+
timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
|
|
73
|
+
working_directory=tmp_path,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert FIXTURE_ENCODING_KEYWORD_NAME not in all_observed_runner_keywords
|
|
77
|
+
assert FIXTURE_ERRORS_KEYWORD_NAME not in all_observed_runner_keywords
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Behavioral tests for text codec keyword forwarding in worker-spawn headless overrides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import resolve_worker_spawn as dispatcher
|
|
9
|
+
from codec_forwarding_test_support import (
|
|
10
|
+
FIXTURE_CHAIN_ENCODING,
|
|
11
|
+
FIXTURE_CHAIN_ERRORS,
|
|
12
|
+
FIXTURE_ENCODING_KEYWORD_NAME,
|
|
13
|
+
FIXTURE_ERRORS_KEYWORD_NAME,
|
|
14
|
+
install_codec_seams,
|
|
15
|
+
)
|
|
16
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
17
|
+
DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
18
|
+
UTF8_ENCODING,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
FIXTURE_CHAIN_STDOUT = '{"tier":"claude","status":"done"}'
|
|
22
|
+
FIXTURE_PROMPT_TEXT = "do the work"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _install_dispatcher_codec_seams(
|
|
26
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
27
|
+
*,
|
|
28
|
+
all_chain_codec_keywords: dict[str, str],
|
|
29
|
+
) -> dict[str, object]:
|
|
30
|
+
return install_codec_seams(
|
|
31
|
+
monkeypatch,
|
|
32
|
+
all_chain_codec_keywords=all_chain_codec_keywords,
|
|
33
|
+
chain_stdout=FIXTURE_CHAIN_STDOUT,
|
|
34
|
+
runner_host=dispatcher,
|
|
35
|
+
runner_attribute_name="spawn_claude_runner",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_headless_with_prompt(
|
|
40
|
+
*,
|
|
41
|
+
prompt_file: Path,
|
|
42
|
+
working_directory: Path,
|
|
43
|
+
timeout_seconds: int = DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
44
|
+
) -> None:
|
|
45
|
+
prompt_stdin = prompt_file.open(encoding=UTF8_ENCODING)
|
|
46
|
+
try:
|
|
47
|
+
dispatcher._run_claude_with_headless_overrides(
|
|
48
|
+
[],
|
|
49
|
+
timeout_seconds=timeout_seconds,
|
|
50
|
+
working_directory=working_directory,
|
|
51
|
+
prompt_stdin=prompt_stdin,
|
|
52
|
+
)
|
|
53
|
+
finally:
|
|
54
|
+
prompt_stdin.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_forwards_text_codec_keywords_to_subprocess_runner(
|
|
58
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
59
|
+
) -> None:
|
|
60
|
+
prompt_file = tmp_path / "prompt.txt"
|
|
61
|
+
prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding=UTF8_ENCODING)
|
|
62
|
+
all_observed_runner_keywords = _install_dispatcher_codec_seams(
|
|
63
|
+
monkeypatch,
|
|
64
|
+
all_chain_codec_keywords={
|
|
65
|
+
FIXTURE_ENCODING_KEYWORD_NAME: FIXTURE_CHAIN_ENCODING,
|
|
66
|
+
FIXTURE_ERRORS_KEYWORD_NAME: FIXTURE_CHAIN_ERRORS,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
_run_headless_with_prompt(
|
|
71
|
+
prompt_file=prompt_file,
|
|
72
|
+
working_directory=tmp_path,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert (
|
|
76
|
+
all_observed_runner_keywords[FIXTURE_ENCODING_KEYWORD_NAME]
|
|
77
|
+
== FIXTURE_CHAIN_ENCODING
|
|
78
|
+
)
|
|
79
|
+
assert (
|
|
80
|
+
all_observed_runner_keywords[FIXTURE_ERRORS_KEYWORD_NAME]
|
|
81
|
+
== FIXTURE_CHAIN_ERRORS
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_absent_text_codec_keywords_are_not_invented(
|
|
86
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
87
|
+
) -> None:
|
|
88
|
+
prompt_file = tmp_path / "prompt.txt"
|
|
89
|
+
prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding=UTF8_ENCODING)
|
|
90
|
+
all_observed_runner_keywords = _install_dispatcher_codec_seams(
|
|
91
|
+
monkeypatch,
|
|
92
|
+
all_chain_codec_keywords={},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
_run_headless_with_prompt(
|
|
96
|
+
prompt_file=prompt_file,
|
|
97
|
+
working_directory=tmp_path,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert FIXTURE_ENCODING_KEYWORD_NAME not in all_observed_runner_keywords
|
|
101
|
+
assert FIXTURE_ERRORS_KEYWORD_NAME not in all_observed_runner_keywords
|
|
@@ -161,13 +161,18 @@ orchestrating session's own steps, and the script's agent-prompt text carries
|
|
|
161
161
|
```
|
|
162
162
|
Workflow({
|
|
163
163
|
scriptPath: "<this skill dir>/workflow/converge.mjs",
|
|
164
|
-
args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false }
|
|
164
|
+
args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false, homeDirectory: "<HOME>" }
|
|
165
165
|
})
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
`scriptPath` is the absolute path to `workflow/converge.mjs` inside this skill's
|
|
169
169
|
own directory (on this install,
|
|
170
|
-
`<home>/.claude/skills/autoconverge/workflow/converge.mjs`).
|
|
170
|
+
`<home>/.claude/skills/autoconverge/workflow/converge.mjs`). `homeDirectory` is
|
|
171
|
+
the absolute path to the directory that holds `.claude` (resolve it once from
|
|
172
|
+
`$HOME`, or `$env:USERPROFILE` on Windows, with forward slashes). The workflow
|
|
173
|
+
runs in a sandbox with no access to environment variables, so the run reads the
|
|
174
|
+
home directory from this arg to build the path to the codex-review scripts the
|
|
175
|
+
Codex gate calls; leave it out and the Codex gate cannot find those scripts. Set
|
|
171
176
|
`bugbotDisabled: true` only when the user has opted Cursor Bugbot out for the
|
|
172
177
|
run; otherwise the workflow detects an opt-out or an unreachable Bugbot on its
|
|
173
178
|
own. Set `copilotDisabled: true` when the step 5 quota pre-check exits non-zero,
|
|
@@ -189,7 +194,7 @@ Write it again when the result lands, so the handoff carries the final run id an
|
|
|
189
194
|
names the teardown phase the fresh session picks up from.
|
|
190
195
|
|
|
191
196
|
The workflow returns
|
|
192
|
-
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, cleanAuditNote, reuseNote, deferredPrs }`,
|
|
197
|
+
`{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, codexNote, cleanAuditNote, reuseNote, deferredPrs }`,
|
|
193
198
|
plus a `userReview` field on a `blocker: "user-review"` return. `cleanAuditNote` is
|
|
194
199
|
non-null when the environment refused the CLEAN bugteam review post and the run
|
|
195
200
|
recorded the bypass — see
|
|
@@ -359,6 +364,7 @@ ready again — then run the checkpoints.
|
|
|
359
364
|
Blocker: <blocker> # only when blocked
|
|
360
365
|
Standards: <standardsNote> # only when a round deferred code-standard findings
|
|
361
366
|
Copilot: <copilotNote> # only when Copilot was down or out of quota
|
|
367
|
+
Codex: <codexNote> # only when the Codex gate was bypassed (codex_down or opt-out)
|
|
362
368
|
Clean-audit: <cleanAuditNote> # only when the CLEAN bugteam post was bypassed
|
|
363
369
|
Reuse: <reuseNote> # only when the reuse pass identified an improvement
|
|
364
370
|
```
|
|
@@ -135,7 +135,8 @@ any bot threads with a deferral note, and reports the deferral in
|
|
|
135
135
|
the weekly usage probe via `is_codex_review_required` (shared threshold
|
|
136
136
|
constant — no inline percent), and the wrapper's `codex_down` class.
|
|
137
137
|
- Opt-out token or `codex_down` → set `codexDown`, no stamp, move to the
|
|
138
|
-
convergence check with `--codex-down`.
|
|
138
|
+
convergence check with `--codex-down`. A `codex_down` classification also
|
|
139
|
+
records a `codexNote` so the skip stays visible in the final report.
|
|
139
140
|
- Usage at/below threshold or null → skip with no stamp; the machine checklist
|
|
140
141
|
applies the same rule.
|
|
141
142
|
- Above threshold → run the codex-review wrapper against the PR base branch.
|
|
@@ -72,6 +72,7 @@ Workflow({
|
|
|
72
72
|
scriptPath: "<this skill dir>/workflow/converge_multi.mjs",
|
|
73
73
|
args: {
|
|
74
74
|
convergeScriptPath: "<this skill dir>/workflow/converge.mjs",
|
|
75
|
+
homeDirectory: "<HOME>",
|
|
75
76
|
prs: [
|
|
76
77
|
{ owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false, copilotDisabled: false },
|
|
77
78
|
{ owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false, copilotDisabled: false }
|
|
@@ -82,7 +83,11 @@ Workflow({
|
|
|
82
83
|
|
|
83
84
|
`convergeScriptPath` is the absolute path to `workflow/converge.mjs` in this same
|
|
84
85
|
skill directory; each `repoPath` is the absolute path of the worktree that PR is
|
|
85
|
-
checked out in.
|
|
86
|
+
checked out in. `homeDirectory` is the absolute path to the directory that holds
|
|
87
|
+
`.claude` (resolve it once from `$HOME`, or `$env:USERPROFILE` on Windows, with
|
|
88
|
+
forward slashes); the fan-out forwards it to every child run, which needs it to
|
|
89
|
+
build the codex-review scripts path because the workflow sandbox has no access to
|
|
90
|
+
environment variables. The workflow runs in the background and notifies the session on
|
|
86
91
|
completion; watch live progress with `/workflows`, where each PR's child run
|
|
87
92
|
appears under its own group.
|
|
88
93
|
|
|
@@ -88,16 +88,22 @@ Each reviewer-down condition below skips its own convergence-check gate. The fla
|
|
|
88
88
|
`--copilot-down` (the Copilot review gate and the pending-requested-reviews
|
|
89
89
|
gate bypassed), then mark-ready. `copilotNote` records the bypass for the
|
|
90
90
|
final report.
|
|
91
|
-
- **Terminal Codex gate skipped or
|
|
92
|
-
runs after Bugbot and Copilot. When
|
|
93
|
-
(`reviews_disabled.py --reviewer codex`
|
|
94
|
-
advances with no review. When the weekly
|
|
95
|
-
null or at/below the shared threshold
|
|
96
|
-
gate skips without a stamp; the
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
- **Terminal Codex gate skipped (opt-out, usage, or `codex_down`)** — the
|
|
92
|
+
conditional-required Codex gate runs after Bugbot and Copilot. When
|
|
93
|
+
`CLAUDE_REVIEWS_DISABLED` lists `codex` (`reviews_disabled.py --reviewer codex`
|
|
94
|
+
exit 0), the gate sets `codexDown` and advances with no review. When the weekly
|
|
95
|
+
usage probe reports `percent_left` null or at/below the shared threshold
|
|
96
|
+
(`is_codex_review_required` false), the gate skips without a stamp; the
|
|
97
|
+
convergence check applies the same rule. When the codex-review wrapper
|
|
98
|
+
classifies `codex_down` — a Codex CLI error, or a review that ran past its
|
|
99
|
+
`CONFIG.codexReviewTimeoutSeconds` budget — the gate skips the same way: it sets
|
|
100
|
+
`codexDown`, records a `codexNote` so the skip stays visible in the final
|
|
101
|
+
report, clears `codexCleanAt`, and advances to `FINALIZE` (mark-ready with the
|
|
102
|
+
gate bypassed) so a Codex outage does not block convergence; Codex runs again on
|
|
103
|
+
the next convergence. A clean required run stamps `codexCleanAt` for
|
|
104
|
+
`--codex-clean-at`. Non-code-standard findings re-enter `CONVERGE` through the
|
|
105
|
+
existing fix path. Standards-only findings defer a follow-up, stamp
|
|
106
|
+
`codexCleanAt`, and advance to `FINALIZE` (no fix push).
|
|
101
107
|
- **A lens agent dies** — when one parallel reading lens returns null (a terminal
|
|
102
108
|
agent failure), the round proceeds on the surviving lenses. A real defect it
|
|
103
109
|
would have caught surfaces in a later round or at the convergence check. A dead
|
|
@@ -655,7 +655,7 @@ test('the workflow declares cleanAuditNote and assembles it into every result vi
|
|
|
655
655
|
assert.match(convergeSource, /let cleanAuditNote = null/);
|
|
656
656
|
assert.match(
|
|
657
657
|
convergeSource,
|
|
658
|
-
/const assembleResult = \(outcomeFields\) => \(\{\n\s*\.\.\.outcomeFields,\n\s*standardsNote,\n\s*copilotNote,\n\s*cleanAuditNote,\n\s*reuseNote,\n\s*deferredPrs,\n\s*\}\)/,
|
|
658
|
+
/const assembleResult = \(outcomeFields\) => \(\{\n\s*\.\.\.outcomeFields,\n\s*standardsNote,\n\s*copilotNote,\n\s*codexNote,\n\s*cleanAuditNote,\n\s*reuseNote,\n\s*deferredPrs,\n\s*\}\)/,
|
|
659
659
|
);
|
|
660
660
|
const assembleCalls = convergeSource.match(/return assembleResult\(/g) || [];
|
|
661
661
|
assert.ok(
|