claude-dev-env 2.27.0 → 2.28.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 (109) hide show
  1. package/.agents/skills/hitl/SKILL.md +45 -0
  2. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  4. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/docs/CODE_RULES.md +3 -3
  7. package/docs/agent-spawn-protocol.md +1 -1
  8. package/docs/references/prose-style-enforcement.md +5 -9
  9. package/hooks/blocking/AGENTS.md +3 -13
  10. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  11. package/hooks/blocking/code_rules_enforcer.py +0 -121
  12. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  13. package/hooks/blocking/code_rules_shared.py +23 -0
  14. package/hooks/blocking/code_rules_test_layout.py +8 -8
  15. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  16. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  17. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  18. package/hooks/blocking/state_description_blocker.py +1 -7
  19. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  20. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  21. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  22. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  23. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  24. package/hooks/blocking/test_code_rules_shared.py +12 -0
  25. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  26. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  27. package/hooks/blocking/test_state_description_blocker.py +6 -4
  28. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  29. package/hooks/hooks_constants/AGENTS.md +1 -6
  30. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  31. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  32. package/hooks/hooks_constants/messages.py +0 -2
  33. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  34. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  35. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  36. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  37. package/hooks/hooks_constants/test_messages.py +5 -3
  38. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  39. package/package.json +1 -1
  40. package/rules/AGENTS.md +3 -6
  41. package/rules/claims-as-quotes.md +0 -10
  42. package/rules/code-standards.md +6 -6
  43. package/rules/explore-thoroughly.md +0 -1
  44. package/rules/failure-blast-radius.md +0 -8
  45. package/rules/falsify-before-green.md +0 -8
  46. package/rules/file-global-constants.md +2 -2
  47. package/rules/filesystem-search.md +1 -1
  48. package/rules/git-workflow.md +1 -9
  49. package/rules/hedging-claims.md +2 -6
  50. package/rules/long-horizon-autonomy.md +1 -1
  51. package/rules/measurement-denominators.md +0 -9
  52. package/rules/research-mode.md +0 -6
  53. package/rules/verify-before-asking.md +0 -5
  54. package/rules/verify-runtime-state.md +0 -5
  55. package/scripts/codex_compat_materializer.py +99 -24
  56. package/scripts/sync_to_cursor/rules.py +0 -10
  57. package/scripts/tests/test_codex_compat_materializer.py +70 -19
  58. package/scripts/tests/test_engine.py +0 -1
  59. package/scripts/tests/test_rules.py +0 -1
  60. package/scripts/tests/test_sync_to_cursor.py +0 -1
  61. package/system-prompts/software-engineer.xml +3 -3
  62. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  63. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  64. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  65. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  66. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  67. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  68. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  69. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  70. package/hooks/blocking/hedging_language_blocker.py +0 -221
  71. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  72. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  73. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  74. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  75. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  78. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  91. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  92. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  93. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  94. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  95. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  96. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  97. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  98. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  99. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  100. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  101. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  102. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  103. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  104. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  105. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  106. package/rules/conservative-action.md +0 -17
  107. package/rules/context7.md +0 -8
  108. package/rules/few-words.md +0 -3
  109. package/rules/parallel-tools.md +0 -23
@@ -2,7 +2,7 @@ import json
2
2
  import subprocess
3
3
  import sys
4
4
  from pathlib import Path
5
- from typing import Any
5
+ from typing import Any, cast
6
6
 
7
7
  import pytest
8
8
  import tomllib
@@ -30,6 +30,23 @@ from codex_compat_materializer import (
30
30
  )
31
31
 
32
32
 
33
+ def _required_manifest_path(config: MaterializerConfig) -> Path:
34
+ assert config.manifest_path is not None
35
+ return config.manifest_path
36
+
37
+
38
+ def _manifest_files(manifest_path: Path) -> dict[str, dict[str, object]]:
39
+ return cast(dict[str, dict[str, object]], load_manifest(manifest_path)["files"])
40
+
41
+
42
+ def _hook_group(matcher: str, *all_hook_commands: str) -> dict[str, object]:
43
+ return {"matcher": matcher, "hooks": [{"type": "command", "command": each_command} for each_command in all_hook_commands]}
44
+
45
+
46
+ def _hook_commands(manifest: dict[str, Any], event_name: str) -> list[str]:
47
+ return [each_hook["command"] for each_group in manifest["hooks"][event_name] for each_hook in each_group["hooks"]]
48
+
49
+
33
50
  def test_conversion_escapes_toml_content() -> None:
34
51
  agent = parse_frontmatter(Path("agent.md"), '---\nname: sample\ndescription: "say \\"hi\\""\ntools: ["Read", "Write"]\n---\n', "agent.md")
35
52
  converted = convert_agent(agent)
@@ -196,7 +213,7 @@ def test_case_folded_target_collisions_include_existing_and_manifest(tmp_path: P
196
213
  def test_manifest_target_collision_is_rejected(tmp_path: Path) -> None:
197
214
  config = MaterializerConfig(tmp_path / "source", tmp_path / "target", should_apply=True)
198
215
  planned = [PlannedFile("a.md", "Luna.toml", "managed", "hash")]
199
- atomic_write(config.manifest_path, '{"files": {"luna.TOML": {}}, "version": 1}\n')
216
+ atomic_write(_required_manifest_path(config), '{"files": {"luna.TOML": {}}, "version": 1}\n')
200
217
  with pytest.raises(MaterializerError):
201
218
  publish_plan(config, planned)
202
219
 
@@ -206,12 +223,12 @@ def test_first_run_manifest_target_collision_is_rejected(tmp_path: Path) -> None
206
223
  target = tmp_path / "target"
207
224
  source.mkdir()
208
225
  config = MaterializerConfig(source, target, should_apply=True)
209
- planned = [PlannedFile("a.md", config.manifest_path.name, "managed", "hash")]
226
+ planned = [PlannedFile("a.md", _required_manifest_path(config).name, "managed", "hash")]
210
227
 
211
228
  with pytest.raises(MaterializerError, match="compatibility manifest"):
212
229
  publish_plan(config, planned)
213
230
 
214
- assert not config.manifest_path.exists()
231
+ assert not _required_manifest_path(config).exists()
215
232
  assert not config.target_root.exists()
216
233
 
217
234
 
@@ -238,11 +255,11 @@ def test_dry_run_is_non_mutating_and_writes_are_idempotent(tmp_path: Path) -> No
238
255
  planned, report = build_plan(apply_config)
239
256
  publish_plan(apply_config, planned, report)
240
257
  first = (target / "Luna.toml").read_text(encoding="utf-8")
241
- first_manifest = apply_config.manifest_path.read_bytes()
258
+ first_manifest = _required_manifest_path(apply_config).read_bytes()
242
259
  planned, report = build_plan(apply_config)
243
260
  second_report = publish_plan(apply_config, planned, report)
244
261
  assert (target / "Luna.toml").read_text(encoding="utf-8") == first
245
- assert apply_config.manifest_path.read_bytes() == first_manifest
262
+ assert _required_manifest_path(apply_config).read_bytes() == first_manifest
246
263
  assert second_report.written == 0
247
264
  assert second_report.deleted == 0
248
265
  assert second_report.errors == 0
@@ -292,13 +309,13 @@ def test_manifest_is_published_last_and_previous_manifest_survives_failure(tmp_p
292
309
  target = tmp_path / "target"
293
310
  source.mkdir()
294
311
  config = MaterializerConfig(source, target, should_apply=True)
295
- manifest = config.manifest_path
312
+ manifest = _required_manifest_path(config)
296
313
  manifest.parent.mkdir()
297
314
  atomic_write(manifest, '{"files": {}, "version": 1}\n')
298
315
  planned = [PlannedFile("agent.md", "Luna.toml", "managed", "hash")]
299
316
  with pytest.raises(RuntimeError):
300
317
  publish_plan(config, planned, failure_injector=lambda _: (_ for _ in ()).throw(RuntimeError("stop")))
301
- assert load_manifest(manifest)["files"] == {}
318
+ assert _manifest_files(manifest) == {}
302
319
  assert not (target / "Luna.toml").exists()
303
320
 
304
321
 
@@ -399,7 +416,7 @@ def test_report_contains_deterministic_category_details_and_error_count(
399
416
  assert (target / "Luna.toml").read_text(encoding="utf-8") == "new"
400
417
 
401
418
  (target / "Luna.toml").write_text("changed", encoding="utf-8")
402
- atomic_write(config.manifest_path, json.dumps({"version": 1, "files": {
419
+ atomic_write(_required_manifest_path(config), json.dumps({"version": 1, "files": {
403
420
  "Luna.toml": {"hash": hash_content(managed_content)}
404
421
  }}))
405
422
  stale_report = publish_plan(pruning_config, [])
@@ -407,7 +424,7 @@ def test_report_contains_deterministic_category_details_and_error_count(
407
424
  assert stale_report.details["stale_managed"] == []
408
425
 
409
426
  (target / "Luna.toml").write_text(managed_content, encoding="utf-8")
410
- atomic_write(config.manifest_path, json.dumps({"version": 1, "files": {
427
+ atomic_write(_required_manifest_path(config), json.dumps({"version": 1, "files": {
411
428
  "Luna.toml": {"hash": hash_content(managed_content)}
412
429
  }}))
413
430
  deleted_report = publish_plan(pruning_config, [])
@@ -423,7 +440,7 @@ def test_report_contains_deterministic_category_details_and_error_count(
423
440
  assert collision_report.details["conflicted"] == ["Nova.toml"]
424
441
 
425
442
  manifest = {"version": 1, "files": {"Missing.toml": {"hash": hash_content("missing")}}}
426
- atomic_write(config.manifest_path, json.dumps(manifest))
443
+ atomic_write(_required_manifest_path(config), json.dumps(manifest))
427
444
  error_report = publish_plan(pruning_config, [])
428
445
  assert error_report.errors == 1
429
446
  assert error_report.error_details == ["missing managed path: Missing.toml"]
@@ -582,7 +599,7 @@ def test_reapply_rewrites_a_managed_file_whose_source_description_changed(
582
599
  assert 'description = "Light"' not in published_text
583
600
  assert refreshed_report.written == 1
584
601
  assert refreshed_report.conflicted == 0
585
- assert load_manifest(config.manifest_path)["files"]["Luna.toml"]["hash"] == hash_content(
602
+ assert _manifest_files(_required_manifest_path(config))["Luna.toml"]["hash"] == hash_content(
586
603
  published_text
587
604
  )
588
605
 
@@ -624,7 +641,7 @@ def test_crash_orphan_matching_the_plan_is_adopted_into_the_manifest(tmp_path: P
624
641
  assert adoption_report.errors == 0
625
642
  assert adoption_report.details["adopted"] == ["Nova.toml"]
626
643
  assert (target / "Nova.toml").read_text(encoding="utf-8") == orphan_content
627
- assert load_manifest(config.manifest_path)["files"]["Nova.toml"]["hash"] == hash_content(
644
+ assert _manifest_files(_required_manifest_path(config))["Nova.toml"]["hash"] == hash_content(
628
645
  orphan_content
629
646
  )
630
647
 
@@ -806,7 +823,7 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
806
823
 
807
824
  projected_path = target / "AGENTS.md"
808
825
  first_projection = projected_path.read_text(encoding="utf-8")
809
- manifest_record = load_manifest(config.manifest_path)["files"]["AGENTS.md"]
826
+ manifest_record = _manifest_files(_required_manifest_path(config))["AGENTS.md"]
810
827
  assert manifest_record["ownership"] == "codex-compat"
811
828
  assert manifest_record["source"] == "rules/failure-blast-radius.md"
812
829
 
@@ -871,6 +888,7 @@ def _prepare_projection_fixture(
871
888
  existing_hooks = {
872
889
  "hooks": {
873
890
  "PreToolUse": [
891
+ {"matcher": "apply_patch"},
874
892
  {
875
893
  "matcher": "Write|Edit",
876
894
  "hooks": [{"type": "command", "command": "python existing.py"}],
@@ -894,6 +912,28 @@ def _prepare_projection_fixture(
894
912
  ],
895
913
  },
896
914
  ],
915
+ "SessionStart": [_hook_group(
916
+ "",
917
+ "python3 %CODEX_HOME%\\hooks\\session\\untracked_repo_detector.py",
918
+ "python3 $env:CODEX_HOME/hooks/session/untracked_repo_detector.py",
919
+ "python3 ${env:CODEX_HOME}/hooks/session/untracked_repo_detector.py",
920
+ "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/fix_worktree_hookspath.py",
921
+ )],
922
+ "UserPromptSubmit": [_hook_group(
923
+ "",
924
+ "python3 /home/example/custom/hooks/session/untracked_repo_detector.py",
925
+ "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/untracked_repo_detector.py.bak",
926
+ )],
927
+ "ConfigChange": [
928
+ None,
929
+ {"matcher": "partial"},
930
+ {"matcher": "malformed", "hooks": "not a list"},
931
+ {"matcher": "malformed-hook", "hooks": [{"command": 7}]},
932
+ {"hooks": [{"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/untracked_repo_detector.py"}]},
933
+ _hook_group(
934
+ "retired", "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/untracked_repo_detector.py"
935
+ ),
936
+ ],
897
937
  "Bash": [{"matcher": "", "hooks": []}],
898
938
  }
899
939
  }
@@ -938,7 +978,8 @@ def _assert_projection_and_manifest(
938
978
  projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
939
979
  assert "Three real attempts, then park." in projected_instruction
940
980
  manifest = json.loads((target / "hooks.json").read_text(encoding="utf-8"))
941
- assert manifest["hooks"]["PreToolUse"][0] == existing_hooks["hooks"]["PreToolUse"][0]
981
+ assert manifest["hooks"]["PreToolUse"][0]["matcher"] == "apply_patch"
982
+ assert manifest["hooks"]["PreToolUse"][1] == existing_hooks["hooks"]["PreToolUse"][1]
942
983
  apply_patch_entries = [
943
984
  each_entry
944
985
  for each_entry in manifest["hooks"]["PreToolUse"]
@@ -953,7 +994,15 @@ def _assert_projection_and_manifest(
953
994
  f'python3 "{target / "hooks" / "blocking" / "code_rules_enforcer.py"}"'
954
995
  )
955
996
  assert managed_command in apply_patch_commands
956
- manifest_record = load_manifest(target / ".codex-compat-manifest.json")["files"]
997
+ expected_commands_by_event = {
998
+ "SessionStart": ["python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/fix_worktree_hookspath.py"],
999
+ "UserPromptSubmit": ["python3 /home/example/custom/hooks/session/untracked_repo_detector.py", "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/untracked_repo_detector.py.bak"],
1000
+ }
1001
+ for each_event_name, expected_commands in expected_commands_by_event.items():
1002
+ assert _hook_commands(manifest, each_event_name) == expected_commands
1003
+ assert manifest["hooks"]["ConfigChange"] == existing_hooks["hooks"]["ConfigChange"][:5]
1004
+ assert manifest["hooks"]["Bash"] == existing_hooks["hooks"]["Bash"]
1005
+ manifest_record = _manifest_files(target / ".codex-compat-manifest.json")
957
1006
  assert manifest_record["AGENTS.md"]["ownership"] == "codex-compat"
958
1007
  assert manifest_record["hooks.json"]["source"] == "hooks/hooks.json"
959
1008
 
@@ -984,12 +1033,14 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
984
1033
  assert allow_run.returncode == 0
985
1034
  assert allow_run.stdout == ""
986
1035
 
1036
+ allow_tool_input = allow_payload["tool_input"]
1037
+ assert isinstance(allow_tool_input, dict)
1038
+ allow_command = allow_tool_input["command"]
1039
+ assert isinstance(allow_command, str)
987
1040
  deny_payload = {
988
1041
  **allow_payload,
989
1042
  "tool_input": {
990
- "command": allow_payload["tool_input"]["command"].replace(
991
- "AssetItemBlocked", "RuntimeError"
992
- )
1043
+ "command": allow_command.replace("AssetItemBlocked", "RuntimeError")
993
1044
  },
994
1045
  }
995
1046
  deny_run = subprocess.run(
@@ -46,7 +46,6 @@ def _write_minimal_curated_rules(rules_directory: Path) -> None:
46
46
  encoding="utf-8",
47
47
  )
48
48
  (rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
49
- (rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
50
49
  (rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
51
50
 
52
51
 
@@ -30,7 +30,6 @@ def _write_minimal_curated_rules(rules_directory: Path) -> None:
30
30
  encoding="utf-8",
31
31
  )
32
32
  (rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
33
- (rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
34
33
  (rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
35
34
 
36
35
 
@@ -44,7 +44,6 @@ def _minimal_rule_files(claude_rules: Path) -> None:
44
44
  encoding="utf-8",
45
45
  )
46
46
  (claude_rules / "research-mode.md").write_text("# RM\n", encoding="utf-8")
47
- (claude_rules / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
48
47
  (claude_rules / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
49
48
 
50
49
 
@@ -94,9 +94,9 @@
94
94
  </problem_solving>
95
95
 
96
96
  <code_quality>
97
- The checked-in .cursor/BUGBOT.md is the full code-quality review contract.
98
- ~/.claude/docs/CODE_RULES.md is the compact projection for generation load.
99
- The checklist below is a short reminder, not a
97
+ ~/.claude/docs/CODE_RULES.md is the full code-quality review contract, loaded on demand.
98
+ The checked-in .cursor/BUGBOT.md is the pointer file Cursor BugBot reads; it points at
99
+ CODE_RULES.md. The checklist below is a short reminder, not a
100
100
  second canonical source.
101
101
 
102
102
  Naming — full words only: