claude-dev-env 2.5.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/_shared/pr-loop/worker-spawn.md +3 -1
  14. package/agents/CLAUDE.md +1 -1
  15. package/agents/code-verifier.md +36 -7
  16. package/bin/codex-compat.mjs +104 -0
  17. package/bin/codex-compat.test.mjs +51 -0
  18. package/codex-capability-map.json +13 -0
  19. package/docs/CODE_RULES.md +2 -0
  20. package/docs/codex-compatibility.md +25 -0
  21. package/docs/nas-ssh-invocation.md +96 -12
  22. package/docs/references/code-review-enforcement.md +31 -6
  23. package/hooks/blocking/CLAUDE.md +3 -0
  24. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  25. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  26. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  27. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  28. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  29. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  30. package/hooks/blocking/state_description_blocker.py +1 -1
  31. package/hooks/blocking/stop_dispatcher.py +1 -1
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  33. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  34. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  35. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  36. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  37. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  38. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  39. package/hooks/git-hooks/CLAUDE.md +3 -0
  40. package/hooks/git-hooks/conftest.py +30 -0
  41. package/hooks/git-hooks/gate_utils.py +2 -2
  42. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  43. package/hooks/git-hooks/pre_push.py +75 -4
  44. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  45. package/hooks/git-hooks/test_config.py +0 -15
  46. package/hooks/git-hooks/test_gate_utils.py +3 -15
  47. package/hooks/git-hooks/test_pre_commit.py +1 -15
  48. package/hooks/git-hooks/test_pre_push.py +236 -27
  49. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  50. package/hooks/hooks.json +0 -12
  51. package/hooks/hooks_constants/CLAUDE.md +5 -1
  52. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  53. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  54. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  55. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  56. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  57. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  58. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  59. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  60. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  61. package/package.json +4 -2
  62. package/rules/CLAUDE.md +17 -23
  63. package/rules/agent-spawn-protocol.md +6 -6
  64. package/rules/anti-corollary-tests.md +1 -1
  65. package/rules/bdd.md +1 -1
  66. package/rules/cleanup-temp-files.md +10 -4
  67. package/rules/code-standards.md +7 -0
  68. package/rules/conservative-action.md +1 -5
  69. package/rules/context7.md +0 -4
  70. package/rules/destructive-commands.md +47 -0
  71. package/rules/doc-inventory-integrity.md +48 -0
  72. package/rules/doc-prose-cuts.md +58 -0
  73. package/rules/docstring-prose-matches-implementation.md +10 -2
  74. package/rules/durable-post-artifacts.md +0 -4
  75. package/rules/eli11-replies.md +31 -0
  76. package/rules/explore-thoroughly.md +4 -4
  77. package/rules/falsify-before-green.md +68 -0
  78. package/rules/file-global-constants.md +1 -1
  79. package/rules/filesystem-search.md +51 -0
  80. package/rules/gh-cli-conventions.md +27 -0
  81. package/rules/git-workflow.md +26 -0
  82. package/rules/hedging-claims.md +9 -0
  83. package/rules/long-horizon-autonomy.md +0 -4
  84. package/rules/measurement-denominators.md +48 -0
  85. package/rules/nas-ssh-invocation.md +23 -5
  86. package/rules/parallel-tools.md +2 -2
  87. package/rules/plain-illustrative-docstrings.md +3 -7
  88. package/rules/plain-language.md +2 -0
  89. package/rules/proof-of-work-pr-comments.md +0 -4
  90. package/rules/re-stage-before-commit.md +2 -0
  91. package/rules/research-mode.md +10 -0
  92. package/rules/shell-invocation.md +21 -0
  93. package/rules/testing.md +4 -0
  94. package/rules/verified-commit-gate-skip.md +3 -27
  95. package/rules/verify-before-asking.md +5 -0
  96. package/rules/windows-filesystem-safe.md +1 -1
  97. package/rules/workers-done-before-complete.md +4 -0
  98. package/scripts/CLAUDE.md +3 -3
  99. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  100. package/scripts/codex_capability_bridge.py +171 -0
  101. package/scripts/codex_compat_materializer.py +1087 -0
  102. package/scripts/codex_compat_watcher.py +502 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  104. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  105. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
  106. package/scripts/grok_headless_runner.py +213 -16
  107. package/scripts/invoke_code_review.py +11 -4
  108. package/scripts/resolve_worker_spawn.py +56 -10
  109. package/scripts/spawn_grok_batch.py +43 -22
  110. package/scripts/sync_to_cursor/rules.py +0 -10
  111. package/scripts/test_grok_headless_runner.py +592 -10
  112. package/scripts/test_invoke_code_review.py +143 -0
  113. package/scripts/test_invoke_code_review_chain.py +1 -1
  114. package/scripts/test_invoke_code_review_contract.py +1 -1
  115. package/scripts/test_resolve_worker_spawn.py +179 -15
  116. package/scripts/test_spawn_grok_batch.py +225 -22
  117. package/scripts/tests/test_code_review_constants.py +80 -0
  118. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  119. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  120. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  121. package/scripts/tests/test_sync_to_cursor.py +0 -1
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
  123. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
  124. package/skills/autoconverge/workflow/converge.mjs +64 -15
  125. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  126. package/skills/condensing-instructions/SKILL.md +42 -51
  127. package/skills/fresh-branch/CLAUDE.md +1 -1
  128. package/skills/fresh-branch/SKILL.md +5 -6
  129. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  131. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  132. package/skills/grok-spawn/SKILL.md +5 -3
  133. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  134. package/skills/orchestrator/SKILL.md +23 -9
  135. package/skills/orchestrator-refresh/SKILL.md +20 -1
  136. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  137. package/skills/session-log/SKILL.md +1 -1
  138. package/rules/claude-md-orphan-file.md +0 -28
  139. package/rules/cleanup-command-forms.md +0 -23
  140. package/rules/code-reviews.md +0 -11
  141. package/rules/env-var-table-code-drift.md +0 -10
  142. package/rules/gh-body-file.md +0 -5
  143. package/rules/gh-paginate.md +0 -3
  144. package/rules/hook-prose-matches-detector.md +0 -15
  145. package/rules/no-historical-clutter.md +0 -26
  146. package/rules/no-inline-destructive-literals.md +0 -9
  147. package/rules/no-justification-noise.md +0 -61
  148. package/rules/package-inventory-stale-entry.md +0 -25
  149. package/rules/right-sized-engineering.md +0 -28
  150. package/rules/self-contained-docs.md +0 -17
  151. package/rules/shell-invocation-policy.md +0 -5
  152. package/rules/state-what-is.md +0 -25
  153. package/rules/tdd.md +0 -7
@@ -22,15 +22,17 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
22
22
  BUILD_PROFILE_PROMPT_HEADER,
23
23
  CLASSIFICATION_ERROR,
24
24
  CLASSIFICATION_OK,
25
+ CLASSIFICATION_TIMEOUT,
25
26
  CLASSIFICATION_USAGE_LIMIT,
26
27
  DEBUG_FILE_FLAG,
27
28
  DEFAULT_ROLE,
28
- DEFAULT_WORKER_MAX_TURNS,
29
29
  DEFAULT_WORKER_TIMEOUT_SECONDS,
30
30
  DISABLE_WEB_SEARCH_FLAG,
31
31
  DISALLOWED_TOOLS_FLAG,
32
32
  LEADER_SOCKET_FILENAME_PREFIX,
33
33
  LEADER_SOCKET_FILENAME_SUFFIX,
34
+ MAX_TURNS_FLAG,
35
+ MAXIMUM_WORKER_TIMEOUT_SECONDS,
34
36
  OUTPUT_FILENAME_PREFIX,
35
37
  PROMPT_FILENAME_PREFIX,
36
38
  READONLY_DISALLOWED_TOOLS_VALUE,
@@ -53,7 +55,6 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
53
55
  TOOL_PROFILE_READONLY,
54
56
  UTF8_ENCODING,
55
57
  WORKER_SPEC_AGENT_NAME_KEY,
56
- WORKER_SPEC_MAX_TURNS_KEY,
57
58
  WORKER_SPEC_PROMPT_PARTS_KEY,
58
59
  WORKER_SPEC_TIMEOUT_KEY,
59
60
  )
@@ -61,8 +62,10 @@ from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS # noqa: E40
61
62
  from grok_headless_runner import GrokRunnerOutcome # noqa: E402
62
63
  from grok_worker_preflight import PreflightOutcome # noqa: E402
63
64
 
65
+ RETIRED_MAX_TURNS_KEYWORD = "max_turns"
64
66
  FIXTURE_REPORT_TEXT = '{"status":"done","role":"investigator"}'
65
67
  FIXTURE_USAGE_LIMIT_TEXT = "rate limit exceeded (HTTP 429): quota exceeded"
68
+ FIXTURE_TIMEOUT_KILL_TEXT = "worker exceeded its timeout and was killed"
66
69
 
67
70
 
68
71
  def _write_prompt_parts(
@@ -871,7 +874,7 @@ def test_load_batch_spec_missing_worker_keys_raise_value_error(
871
874
  ).lower() or "must be" in str(raised_error.value).lower()
872
875
 
873
876
 
874
- def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
877
+ def test_load_batch_spec_rejects_non_positive_timeout(
875
878
  tmp_path: Path,
876
879
  ) -> None:
877
880
  header_part, body_part = _write_prompt_parts(tmp_path)
@@ -891,27 +894,11 @@ def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
891
894
  zero_timeout_dir,
892
895
  all_worker_payloads=[zero_timeout_payload],
893
896
  )
894
- with pytest.raises(ValueError, match=WORKER_SPEC_TIMEOUT_KEY):
897
+ with pytest.raises(ValueError, match="MIN_WORKER_TIMEOUT_SECONDS"):
895
898
  batch.load_batch_spec(zero_timeout_path)
896
899
 
897
- negative_turns_dir = tmp_path / "negative-turns"
898
- negative_turns_dir.mkdir()
899
- negative_turns_payload = _worker_payload(
900
- role_name="negative-turns",
901
- all_prompt_parts=[str(header_part), str(body_part)],
902
- working_directory=working_directory,
903
- tool_profile=TOOL_PROFILE_BUILD,
904
- )
905
- negative_turns_payload[WORKER_SPEC_MAX_TURNS_KEY] = -1
906
- negative_turns_path = _write_batch_spec(
907
- negative_turns_dir,
908
- all_worker_payloads=[negative_turns_payload],
909
- )
910
- with pytest.raises(ValueError, match=WORKER_SPEC_MAX_TURNS_KEY):
911
- batch.load_batch_spec(negative_turns_path)
912
-
913
900
 
914
- def test_load_batch_spec_accepts_default_timeout_and_max_turns(
901
+ def test_load_batch_spec_accepts_the_default_timeout(
915
902
  tmp_path: Path,
916
903
  ) -> None:
917
904
  header_part, body_part = _write_prompt_parts(tmp_path)
@@ -931,7 +918,223 @@ def test_load_batch_spec_accepts_default_timeout_and_max_turns(
931
918
 
932
919
  assert len(batch_spec.all_workers) == 1
933
920
  assert batch_spec.all_workers[0].timeout_seconds == DEFAULT_WORKER_TIMEOUT_SECONDS
934
- assert batch_spec.all_workers[0].max_turns == DEFAULT_WORKER_MAX_TURNS
921
+
922
+
923
+ def test_timeout_over_the_ceiling_is_refused_and_at_the_ceiling_passes(
924
+ tmp_path: Path,
925
+ ) -> None:
926
+ """The launcher refuses a spec past the 90-minute ceiling; it never clamps.
927
+
928
+ ::
929
+
930
+ timeout_seconds 5401 flag: ValueError naming MAXIMUM_WORKER_TIMEOUT_SECONDS
931
+ timeout_seconds 5400 ok: loads, value untouched
932
+ timeout_seconds 30 ok: loads, value untouched
933
+ """
934
+ header_part, body_part = _write_prompt_parts(tmp_path)
935
+ working_directory = tmp_path / "project"
936
+ working_directory.mkdir()
937
+
938
+ over_ceiling_directory = tmp_path / "over-ceiling"
939
+ over_ceiling_directory.mkdir()
940
+ over_ceiling_path = _write_batch_spec(
941
+ over_ceiling_directory,
942
+ all_worker_payloads=[
943
+ _worker_payload(
944
+ role_name="over-ceiling",
945
+ all_prompt_parts=[str(header_part), str(body_part)],
946
+ working_directory=working_directory,
947
+ tool_profile=TOOL_PROFILE_BUILD,
948
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
949
+ )
950
+ ],
951
+ )
952
+ with pytest.raises(ValueError, match="MAXIMUM_WORKER_TIMEOUT_SECONDS"):
953
+ batch.load_batch_spec(over_ceiling_path)
954
+
955
+ at_ceiling_directory = tmp_path / "at-ceiling"
956
+ at_ceiling_directory.mkdir()
957
+ at_ceiling_path = _write_batch_spec(
958
+ at_ceiling_directory,
959
+ all_worker_payloads=[
960
+ _worker_payload(
961
+ role_name="at-ceiling",
962
+ all_prompt_parts=[str(header_part), str(body_part)],
963
+ working_directory=working_directory,
964
+ tool_profile=TOOL_PROFILE_BUILD,
965
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
966
+ ),
967
+ _worker_payload(
968
+ role_name="well-under-ceiling",
969
+ all_prompt_parts=[str(header_part), str(body_part)],
970
+ working_directory=working_directory,
971
+ tool_profile=TOOL_PROFILE_BUILD,
972
+ timeout_seconds=30,
973
+ ),
974
+ ],
975
+ )
976
+ at_ceiling_spec = batch.load_batch_spec(at_ceiling_path)
977
+
978
+ assert at_ceiling_spec.all_workers[0].timeout_seconds == (
979
+ MAXIMUM_WORKER_TIMEOUT_SECONDS
980
+ )
981
+ assert at_ceiling_spec.all_workers[1].timeout_seconds == 30
982
+
983
+
984
+ def test_ceiling_timeout_reaches_the_runner_untouched(
985
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
986
+ ) -> None:
987
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="long-worker")
988
+ working_directory = tmp_path / "project"
989
+ working_directory.mkdir()
990
+ run_state_directory = tmp_path / "run-state"
991
+ batch_spec = batch.load_batch_spec(
992
+ _write_batch_spec(
993
+ tmp_path,
994
+ all_worker_payloads=[
995
+ _worker_payload(
996
+ role_name="long-worker",
997
+ all_prompt_parts=[str(header_part), str(body_part)],
998
+ working_directory=working_directory,
999
+ tool_profile=TOOL_PROFILE_READONLY,
1000
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
1001
+ )
1002
+ ],
1003
+ )
1004
+ )
1005
+ recorder = _RunnerRecorder({"long-worker": _ok_outcome()})
1006
+ monkeypatch.setattr(
1007
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1008
+ )
1009
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1010
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1011
+
1012
+ batch.run_grok_batch(
1013
+ batch_spec=batch_spec,
1014
+ run_state_directory=run_state_directory,
1015
+ )
1016
+
1017
+ assert recorder.all_keyword_arguments[0]["timeout_seconds"] == (
1018
+ MAXIMUM_WORKER_TIMEOUT_SECONDS
1019
+ )
1020
+
1021
+
1022
+ def test_worker_invocations_carry_no_turn_cap(
1023
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1024
+ ) -> None:
1025
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="uncapped")
1026
+ working_directory = tmp_path / "project"
1027
+ working_directory.mkdir()
1028
+ run_state_directory = tmp_path / "run-state"
1029
+ batch_spec = batch.load_batch_spec(
1030
+ _write_batch_spec(
1031
+ tmp_path,
1032
+ all_worker_payloads=[
1033
+ _worker_payload(
1034
+ role_name="uncapped",
1035
+ all_prompt_parts=[str(header_part), str(body_part)],
1036
+ working_directory=working_directory,
1037
+ tool_profile=TOOL_PROFILE_READONLY,
1038
+ )
1039
+ ],
1040
+ )
1041
+ )
1042
+ recorder = _RunnerRecorder({"uncapped": _ok_outcome()})
1043
+ monkeypatch.setattr(
1044
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1045
+ )
1046
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1047
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1048
+
1049
+ batch.run_grok_batch(
1050
+ batch_spec=batch_spec,
1051
+ run_state_directory=run_state_directory,
1052
+ )
1053
+
1054
+ launched_keyword_arguments = recorder.all_keyword_arguments[0]
1055
+ all_extra_arguments = launched_keyword_arguments["all_extra_arguments"]
1056
+ assert RETIRED_MAX_TURNS_KEYWORD not in launched_keyword_arguments
1057
+ assert isinstance(all_extra_arguments, tuple)
1058
+ assert MAX_TURNS_FLAG not in all_extra_arguments
1059
+
1060
+
1061
+ def test_timed_out_worker_reads_as_timeout_beside_a_completed_worker(
1062
+ monkeypatch: pytest.MonkeyPatch,
1063
+ tmp_path: Path,
1064
+ capsys: pytest.CaptureFixture[str],
1065
+ ) -> None:
1066
+ """A killed worker stays distinguishable from a completed one in the summary.
1067
+
1068
+ ::
1069
+
1070
+ killed worker ok: classification timeout, is_ok False, exit code 1
1071
+ completed worker ok: classification ok, is_ok True
1072
+ """
1073
+ header_done, body_done = _write_prompt_parts(tmp_path, role_marker="done-worker")
1074
+ header_killed, body_killed = _write_prompt_parts(
1075
+ tmp_path, role_marker="killed-worker"
1076
+ )
1077
+ working_directory = tmp_path / "project"
1078
+ working_directory.mkdir()
1079
+ run_state_directory = tmp_path / "run-state"
1080
+ specification_path = _write_batch_spec(
1081
+ tmp_path,
1082
+ all_worker_payloads=[
1083
+ _worker_payload(
1084
+ role_name="done-worker",
1085
+ all_prompt_parts=[str(header_done), str(body_done)],
1086
+ working_directory=working_directory,
1087
+ tool_profile=TOOL_PROFILE_BUILD,
1088
+ ),
1089
+ _worker_payload(
1090
+ role_name="killed-worker",
1091
+ all_prompt_parts=[str(header_killed), str(body_killed)],
1092
+ working_directory=working_directory,
1093
+ tool_profile=TOOL_PROFILE_BUILD,
1094
+ ),
1095
+ ],
1096
+ )
1097
+ recorder = _RunnerRecorder(
1098
+ {
1099
+ "done-worker": _ok_outcome(),
1100
+ "killed-worker": GrokRunnerOutcome(
1101
+ is_ok=False,
1102
+ returncode=-9,
1103
+ classification=CLASSIFICATION_TIMEOUT,
1104
+ stdout="",
1105
+ stderr=FIXTURE_TIMEOUT_KILL_TEXT,
1106
+ ),
1107
+ }
1108
+ )
1109
+ monkeypatch.setattr(
1110
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1111
+ )
1112
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1113
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1114
+
1115
+ exit_code = batch.main(
1116
+ [
1117
+ "--spec",
1118
+ str(specification_path),
1119
+ "--run-temp-dir",
1120
+ str(run_state_directory),
1121
+ ]
1122
+ )
1123
+
1124
+ summary_payload = json.loads(capsys.readouterr().out)
1125
+ payload_by_role_name = {
1126
+ each_payload[SUMMARY_ROLE_NAME_KEY]: each_payload
1127
+ for each_payload in summary_payload[SUMMARY_WORKERS_KEY]
1128
+ }
1129
+ killed_payload = payload_by_role_name["killed-worker"]
1130
+ done_payload = payload_by_role_name["done-worker"]
1131
+
1132
+ assert exit_code == 1
1133
+ assert killed_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_TIMEOUT
1134
+ assert killed_payload[SUMMARY_IS_OK_KEY] is False
1135
+ assert killed_payload[SUMMARY_CLASSIFICATION_KEY] != CLASSIFICATION_OK
1136
+ assert done_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_OK
1137
+ assert done_payload[SUMMARY_IS_OK_KEY] is True
935
1138
 
936
1139
 
937
1140
  def test_unwritable_report_file_keeps_the_worker_outcome(
@@ -0,0 +1,80 @@
1
+ """Specifications for the permission modes the review binary is asked for.
2
+
3
+ The review binary accepts one permission mode for an ordinary caller and a
4
+ different one for root. Naming the wrong mode means the binary refuses the
5
+ call, no review runs, and no stamp is ever minted.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+
13
+ import pytest
14
+
15
+ from dev_env_scripts_constants import code_review_constants
16
+
17
+
18
+ BYPASS_MODE_TOKEN: str = "bypassPermissions"
19
+ ACCEPT_EDITS_MODE_TOKEN: str = "acceptEdits"
20
+ ROOT_USER_ID: int = 0
21
+ UNPRIVILEGED_USER_ID: int = 1000
22
+
23
+
24
+ def _permission_mode_seen_by(
25
+ monkeypatch: pytest.MonkeyPatch, effective_user_id: int
26
+ ) -> str:
27
+ """Read the review permission mode a caller with this user id resolves."""
28
+ monkeypatch.setattr(os, "geteuid", lambda: effective_user_id)
29
+ reloaded_constants = importlib.reload(code_review_constants)
30
+ return str(reloaded_constants.REVIEW_PERMISSION_MODE)
31
+
32
+
33
+ def test_root_detection_agrees_with_this_process(
34
+ monkeypatch: pytest.MonkeyPatch,
35
+ ) -> None:
36
+ monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID)
37
+ reloaded_constants = importlib.reload(code_review_constants)
38
+
39
+ assert reloaded_constants.IS_ROOT_CALLER is True
40
+ assert reloaded_constants.ROOT_EFFECTIVE_USER_ID == ROOT_USER_ID
41
+
42
+
43
+ def test_bypass_mode_names_the_token_the_binary_reads() -> None:
44
+ assert code_review_constants.PERMISSION_MODE_BYPASS == BYPASS_MODE_TOKEN
45
+
46
+
47
+ def test_accept_edits_mode_names_the_token_the_binary_reads() -> None:
48
+ assert code_review_constants.PERMISSION_MODE_ACCEPT_EDITS == ACCEPT_EDITS_MODE_TOKEN
49
+
50
+
51
+ def test_a_root_caller_resolves_the_mode_the_binary_accepts(
52
+ monkeypatch: pytest.MonkeyPatch,
53
+ ) -> None:
54
+ assert _permission_mode_seen_by(monkeypatch, ROOT_USER_ID) == (
55
+ ACCEPT_EDITS_MODE_TOKEN
56
+ )
57
+
58
+
59
+ def test_an_ordinary_caller_resolves_the_bypass_mode(
60
+ monkeypatch: pytest.MonkeyPatch,
61
+ ) -> None:
62
+ assert _permission_mode_seen_by(monkeypatch, UNPRIVILEGED_USER_ID) == (
63
+ BYPASS_MODE_TOKEN
64
+ )
65
+
66
+
67
+ def test_a_platform_without_a_user_id_still_loads_the_constants(
68
+ monkeypatch: pytest.MonkeyPatch,
69
+ ) -> None:
70
+ """Windows exposes no ``os.geteuid``, and the module still imports.
71
+
72
+ Reading a missing ``os.geteuid`` raises, and every caller of the review
73
+ invoker dies at import. A platform that reports no user id is never the
74
+ root the review binary refuses, so it reads as an ordinary caller.
75
+ """
76
+ monkeypatch.delattr(os, "geteuid", raising=False)
77
+ reloaded_constants = importlib.reload(code_review_constants)
78
+
79
+ assert reloaded_constants.IS_ROOT_CALLER is False
80
+ assert reloaded_constants.REVIEW_PERMISSION_MODE == BYPASS_MODE_TOKEN
@@ -0,0 +1,91 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from codex_capability_bridge import translate_capability
9
+
10
+
11
+ @pytest.mark.parametrize("source_surface, expected_target, capability_payload", [("TaskList", "update_plan", {"plan": []}), ("TaskCreate", "update_plan", {"name": "review", "status": "pending"}), ("TaskUpdate", "update_plan", {"name": "review", "status": "pending"}), ("Task", "multi_agent_v1__spawn_agent", {"prompt": "review"}), ("SendMessage", "multi_agent_v1__send_input", {"message": "review"}), ("TaskOutput", "multi_agent_v1__wait_agent", {"task_id": "task-1"}), ("TaskStop", "multi_agent_v1__close_agent", {"task_id": "task-1"})])
12
+ def test_maps_capability(source_surface: str, expected_target: str, capability_payload: dict[str, object]) -> None:
13
+ translated_record = translate_capability(source_surface, capability_payload)
14
+ assert translated_record["target"] == expected_target
15
+
16
+
17
+ def test_normalizes_alias_and_task_payload() -> None:
18
+ translated_record = translate_capability("task_create", {"subject": "write tests", "status": "in-progress"})
19
+ assert translated_record["payload"] == {"plan": [{"step": "write tests", "status": "in_progress"}]}
20
+
21
+
22
+ def test_rejects_bad_payload_and_schedule() -> None:
23
+ assert translate_capability("TaskUpdate", {"name": "x", "status": "later"})["status"] == "malformed"
24
+ assert translate_capability("ScheduleWakeup", {})["status"] == "unsupported"
25
+
26
+
27
+ def test_rejects_unknown_and_private_paths() -> None:
28
+ assert translate_capability("NoSuchSurface", {})["status"] == "unknown"
29
+ assert translate_capability("SendMessage", {"message": "C:\\Users\\melan\\secret"})["status"] == "rejected"
30
+
31
+
32
+ @pytest.mark.parametrize("private_path", ["C:/private/file", "\\\\server\\share\\file", "/private/file", "~/secret", "$HOME/secret", "%USERPROFILE%\\secret", "safe/../secret"])
33
+ def test_rejects_absolute_private_and_traversal_paths(private_path: str) -> None:
34
+ assert translate_capability("SendMessage", {"message": private_path})["status"] == "rejected"
35
+
36
+
37
+ def test_validates_mapped_surface_payloads() -> None:
38
+ assert translate_capability("spawn", {"message": "hello"})["status"] == "malformed"
39
+ assert translate_capability("stop", {"task_id": 3})["status"] == "malformed"
40
+ assert translate_capability("TaskList", {"plan": [{"step": "review"}]})["status"] == "malformed"
41
+
42
+
43
+ def test_translation_is_byte_identical_for_repeated_input() -> None:
44
+ payload = {"message": "hello", "agent_id": "agent-1"}
45
+ first_json = json.dumps(translate_capability("message", payload), sort_keys=True, separators=(",", ":"))
46
+ second_json = json.dumps(translate_capability("message", payload), sort_keys=True, separators=(",", ":"))
47
+ assert first_json == second_json
48
+
49
+
50
+ def test_translation_does_not_execute_payload() -> None:
51
+ payload = {"message": "__import__('subprocess').run('whoami')"}
52
+ translated_record = translate_capability("message", payload)
53
+ assert translated_record["payload"] == payload
54
+
55
+
56
+ def test_translation_is_deterministic_and_cli_is_json() -> None:
57
+ payload = {"message": "hello", "agent_id": "agent-1"}
58
+ first_record = translate_capability("SendMessage", payload)
59
+ second_record = translate_capability("SendMessage", payload)
60
+ assert first_record == second_record
61
+ script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
62
+ cli_run = subprocess.run([sys.executable, str(script_path), "SendMessage", json.dumps(payload)], capture_output=True, text=True, check=True)
63
+ assert json.loads(cli_run.stdout) == first_record
64
+
65
+
66
+ def test_cli_rejects_malformed_payload_with_one_json_error_record() -> None:
67
+ script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
68
+ cli_run = subprocess.run(
69
+ [sys.executable, str(script_path), "SendMessage", "{"],
70
+ capture_output=True,
71
+ text=True,
72
+ )
73
+ assert cli_run.returncode != 0
74
+ assert json.loads(cli_run.stdout) == {
75
+ "source": "unknown",
76
+ "status": "error",
77
+ "error": "payload must be valid JSON",
78
+ }
79
+ assert cli_run.stderr == ""
80
+
81
+
82
+ def test_cli_rejects_malformed_arguments_with_one_json_error_record() -> None:
83
+ script_path = Path(__file__).parents[1] / "codex_capability_bridge.py"
84
+ cli_run = subprocess.run(
85
+ [sys.executable, str(script_path)],
86
+ capture_output=True,
87
+ text=True,
88
+ )
89
+ assert cli_run.returncode != 0
90
+ assert json.loads(cli_run.stdout)["status"] == "error"
91
+ assert cli_run.stderr == ""