claude-dev-env 2.5.0 → 2.7.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 (139) 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/agents/CLAUDE.md +1 -1
  14. package/agents/code-verifier.md +36 -7
  15. package/bin/codex-compat.mjs +104 -0
  16. package/bin/codex-compat.test.mjs +51 -0
  17. package/codex-capability-map.json +13 -0
  18. package/docs/CODE_RULES.md +2 -0
  19. package/docs/codex-compatibility.md +25 -0
  20. package/docs/nas-ssh-invocation.md +96 -12
  21. package/docs/references/code-review-enforcement.md +31 -6
  22. package/hooks/blocking/CLAUDE.md +3 -0
  23. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  24. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  25. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  26. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  27. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  28. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  29. package/hooks/blocking/state_description_blocker.py +1 -1
  30. package/hooks/blocking/stop_dispatcher.py +1 -1
  31. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  32. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  33. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  34. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  35. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  36. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  37. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  38. package/hooks/git-hooks/CLAUDE.md +3 -0
  39. package/hooks/git-hooks/conftest.py +30 -0
  40. package/hooks/git-hooks/gate_utils.py +2 -2
  41. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  42. package/hooks/git-hooks/pre_push.py +75 -4
  43. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  44. package/hooks/git-hooks/test_config.py +0 -15
  45. package/hooks/git-hooks/test_gate_utils.py +3 -15
  46. package/hooks/git-hooks/test_pre_commit.py +1 -15
  47. package/hooks/git-hooks/test_pre_push.py +236 -27
  48. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  49. package/hooks/hooks.json +0 -12
  50. package/hooks/hooks_constants/CLAUDE.md +5 -1
  51. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  52. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  53. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  54. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  55. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  56. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  57. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  58. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  59. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  60. package/package.json +4 -2
  61. package/rules/CLAUDE.md +17 -23
  62. package/rules/agent-spawn-protocol.md +6 -6
  63. package/rules/anti-corollary-tests.md +1 -1
  64. package/rules/bdd.md +1 -1
  65. package/rules/cleanup-temp-files.md +10 -4
  66. package/rules/code-standards.md +7 -0
  67. package/rules/conservative-action.md +1 -5
  68. package/rules/context7.md +0 -4
  69. package/rules/destructive-commands.md +47 -0
  70. package/rules/doc-inventory-integrity.md +48 -0
  71. package/rules/doc-prose-cuts.md +58 -0
  72. package/rules/docstring-prose-matches-implementation.md +10 -2
  73. package/rules/durable-post-artifacts.md +0 -4
  74. package/rules/eli11-replies.md +31 -0
  75. package/rules/explore-thoroughly.md +4 -4
  76. package/rules/falsify-before-green.md +68 -0
  77. package/rules/file-global-constants.md +1 -1
  78. package/rules/filesystem-search.md +51 -0
  79. package/rules/gh-cli-conventions.md +27 -0
  80. package/rules/git-workflow.md +26 -0
  81. package/rules/hedging-claims.md +9 -0
  82. package/rules/long-horizon-autonomy.md +0 -4
  83. package/rules/measurement-denominators.md +48 -0
  84. package/rules/nas-ssh-invocation.md +23 -5
  85. package/rules/parallel-tools.md +2 -2
  86. package/rules/plain-illustrative-docstrings.md +3 -7
  87. package/rules/plain-language.md +2 -0
  88. package/rules/proof-of-work-pr-comments.md +0 -4
  89. package/rules/re-stage-before-commit.md +2 -0
  90. package/rules/research-mode.md +10 -0
  91. package/rules/shell-invocation.md +21 -0
  92. package/rules/testing.md +4 -0
  93. package/rules/verified-commit-gate-skip.md +3 -27
  94. package/rules/verify-before-asking.md +5 -0
  95. package/rules/windows-filesystem-safe.md +1 -1
  96. package/rules/workers-done-before-complete.md +4 -0
  97. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  98. package/scripts/codex_capability_bridge.py +171 -0
  99. package/scripts/codex_compat_materializer.py +1087 -0
  100. package/scripts/codex_compat_watcher.py +502 -0
  101. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  102. package/scripts/invoke_code_review.py +11 -4
  103. package/scripts/sync_to_cursor/rules.py +0 -10
  104. package/scripts/test_invoke_code_review.py +143 -0
  105. package/scripts/test_invoke_code_review_chain.py +1 -1
  106. package/scripts/test_invoke_code_review_contract.py +1 -1
  107. package/scripts/tests/test_code_review_constants.py +80 -0
  108. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  109. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  110. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  111. package/scripts/tests/test_sync_to_cursor.py +0 -1
  112. package/skills/autoconverge/workflow/converge.mjs +1 -1
  113. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  114. package/skills/condensing-instructions/SKILL.md +42 -51
  115. package/skills/fresh-branch/CLAUDE.md +1 -1
  116. package/skills/fresh-branch/SKILL.md +5 -6
  117. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  118. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  119. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  120. package/skills/orchestrator/SKILL.md +23 -9
  121. package/skills/orchestrator-refresh/SKILL.md +20 -1
  122. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  123. package/skills/session-log/SKILL.md +1 -1
  124. package/rules/claude-md-orphan-file.md +0 -28
  125. package/rules/cleanup-command-forms.md +0 -23
  126. package/rules/code-reviews.md +0 -11
  127. package/rules/env-var-table-code-drift.md +0 -10
  128. package/rules/gh-body-file.md +0 -5
  129. package/rules/gh-paginate.md +0 -3
  130. package/rules/hook-prose-matches-detector.md +0 -15
  131. package/rules/no-historical-clutter.md +0 -26
  132. package/rules/no-inline-destructive-literals.md +0 -9
  133. package/rules/no-justification-noise.md +0 -61
  134. package/rules/package-inventory-stale-entry.md +0 -25
  135. package/rules/right-sized-engineering.md +0 -28
  136. package/rules/self-contained-docs.md +0 -17
  137. package/rules/shell-invocation-policy.md +0 -5
  138. package/rules/state-what-is.md +0 -25
  139. package/rules/tdd.md +0 -7
@@ -0,0 +1,143 @@
1
+ """Specifications for what a blocked mint tells the person it blocked.
2
+
3
+ Enforcement refuses every push until a clean stamp covers the branch surface,
4
+ and ``invoke_code_review.py --record-stamp`` is the only way to mint one. When
5
+ that call cannot run, the reason it names is the person's only route back to a
6
+ working push.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ import invoke_code_review as invoker
16
+ from claude_chain_runner import ChainConfigurationError, ChainInvocationOutcome
17
+ from _code_review_test_support import FIXTURE_SESSION_OPUS
18
+ from dev_env_scripts_constants.code_review_constants import (
19
+ DEFAULT_CODE_REVIEW_EFFORT,
20
+ PERMISSION_MODE_ACCEPT_EDITS,
21
+ PERMISSION_MODE_BYPASS,
22
+ REVIEW_PERMISSION_MODE,
23
+ )
24
+
25
+
26
+ def test_review_arguments_carry_the_permission_mode_this_caller_resolves() -> None:
27
+ """The review command asks for a permission mode the binary accepts here.
28
+
29
+ The binary refuses the bypass mode outright for a root caller, so asking
30
+ for it there means no review runs and no stamp is ever minted.
31
+ """
32
+ all_arguments = invoker.build_code_review_arguments(DEFAULT_CODE_REVIEW_EFFORT)
33
+
34
+ assert REVIEW_PERMISSION_MODE in all_arguments
35
+
36
+
37
+ def test_the_resolved_permission_mode_is_one_the_binary_knows() -> None:
38
+ assert REVIEW_PERMISSION_MODE in (
39
+ PERMISSION_MODE_ACCEPT_EDITS,
40
+ PERMISSION_MODE_BYPASS,
41
+ )
42
+
43
+
44
+ CHAIN_CONFIG_REMEDY_TEXT: str = (
45
+ "Claude chain config not found at the path this specification names. "
46
+ "Copy the example config there and list your account binaries."
47
+ )
48
+ HOST_PROFILE_FAILURE_TEXT: str = "session model alias carries no host profile"
49
+ MINT_TIMEOUT_SECONDS: int = 1
50
+ SERVED_COMMAND_NAME: str = "claude"
51
+ REVIEW_BINARY_REFUSAL_TEXT: str = (
52
+ "--dangerously-skip-permissions cannot be used with root privileges"
53
+ )
54
+ REVIEW_FAILURE_RETURNCODE: int = 1
55
+ EMPTY_REVIEW_STDOUT: str = ""
56
+ ROOT_USER_ID: int = 0
57
+ UNPRIVILEGED_USER_ID: int = 1000
58
+
59
+
60
+ def _serve_a_refusing_binary(
61
+ *_all_positional: object, **_all_keyword: object
62
+ ) -> ChainInvocationOutcome:
63
+ return ChainInvocationOutcome(
64
+ served_command=SERVED_COMMAND_NAME,
65
+ returncode=REVIEW_FAILURE_RETURNCODE,
66
+ stdout=EMPTY_REVIEW_STDOUT,
67
+ stderr=REVIEW_BINARY_REFUSAL_TEXT,
68
+ attempts=(),
69
+ )
70
+
71
+
72
+ def test_failed_review_reports_what_the_served_binary_wrote(
73
+ monkeypatch: pytest.MonkeyPatch,
74
+ tmp_path: Path,
75
+ capsys: pytest.CaptureFixture[str],
76
+ ) -> None:
77
+ """A review that a served binary refused names the refusal.
78
+
79
+ The binary can decline for reasons the caller must act on, such as a
80
+ permission mode it will not accept. Dropping its words leaves a bare
81
+ exit code, and no stamp is minted either way.
82
+ """
83
+ monkeypatch.setattr(
84
+ invoker, "_run_claude_with_empty_stdin", _serve_a_refusing_binary
85
+ )
86
+
87
+ outcome = invoker._run_chain_review(
88
+ working_directory=tmp_path,
89
+ timeout_seconds=MINT_TIMEOUT_SECONDS,
90
+ effort=DEFAULT_CODE_REVIEW_EFFORT,
91
+ )
92
+
93
+ captured_streams = capsys.readouterr()
94
+ assert outcome.returncode == REVIEW_FAILURE_RETURNCODE
95
+ assert REVIEW_BINARY_REFUSAL_TEXT in captured_streams.err
96
+
97
+
98
+ def test_missing_chain_config_reports_its_remedy_on_stderr(
99
+ monkeypatch: pytest.MonkeyPatch,
100
+ tmp_path: Path,
101
+ capsys: pytest.CaptureFixture[str],
102
+ ) -> None:
103
+ def raise_chain_configuration_error(**_all_keyword_arguments: object) -> None:
104
+ raise ChainConfigurationError(CHAIN_CONFIG_REMEDY_TEXT)
105
+
106
+ monkeypatch.setattr(
107
+ invoker, "invoke_code_review_and_record_stamp", raise_chain_configuration_error
108
+ )
109
+
110
+ outcome = invoker._mint_or_config_outcome(
111
+ working_directory=tmp_path,
112
+ session_model=FIXTURE_SESSION_OPUS,
113
+ timeout_seconds=MINT_TIMEOUT_SECONDS,
114
+ effort=DEFAULT_CODE_REVIEW_EFFORT,
115
+ )
116
+
117
+ captured_streams = capsys.readouterr()
118
+ assert outcome.is_stamp_minted is False
119
+ assert CHAIN_CONFIG_REMEDY_TEXT in captured_streams.err
120
+
121
+
122
+ def test_host_profile_failure_reports_its_reason_on_stderr(
123
+ monkeypatch: pytest.MonkeyPatch,
124
+ tmp_path: Path,
125
+ capsys: pytest.CaptureFixture[str],
126
+ ) -> None:
127
+ def raise_host_profile_error(**_all_keyword_arguments: object) -> None:
128
+ raise ValueError(HOST_PROFILE_FAILURE_TEXT)
129
+
130
+ monkeypatch.setattr(
131
+ invoker, "invoke_code_review_and_record_stamp", raise_host_profile_error
132
+ )
133
+
134
+ outcome = invoker._mint_or_config_outcome(
135
+ working_directory=tmp_path,
136
+ session_model=FIXTURE_SESSION_OPUS,
137
+ timeout_seconds=MINT_TIMEOUT_SECONDS,
138
+ effort=DEFAULT_CODE_REVIEW_EFFORT,
139
+ )
140
+
141
+ captured_streams = capsys.readouterr()
142
+ assert outcome.is_stamp_minted is False
143
+ assert HOST_PROFILE_FAILURE_TEXT in captured_streams.err
@@ -19,7 +19,7 @@ from _code_review_test_support import (
19
19
  from dev_env_scripts_constants.code_review_constants import (
20
20
  CODE_REVIEW_MODEL_ALIAS,
21
21
  DEFAULT_CODE_REVIEW_EFFORT,
22
- PERMISSION_MODE_BYPASS,
22
+ REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
23
23
  PERMISSION_MODE_FLAG,
24
24
  )
25
25
  from dev_env_scripts_constants.grok_worker_constants import (
@@ -33,7 +33,7 @@ from dev_env_scripts_constants.code_review_constants import (
33
33
  MAXIMUM_STAMP_MINT_PASSES,
34
34
  MODE_CHAIN,
35
35
  MODE_IN_SESSION,
36
- PERMISSION_MODE_BYPASS,
36
+ REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
37
37
  PERMISSION_MODE_FLAG,
38
38
  RESULT_KEY_BOUND_HASH,
39
39
  RESULT_KEY_DIRTY_TREE,
@@ -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 == ""