claude-dev-env 8.0.0 → 8.2.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 (97) hide show
  1. package/.agents/agents/clean-coder.md +4 -4
  2. package/.agents/agents/code-quality-agent.md +4 -4
  3. package/.agents/agents/plan-packet-validator.md +2 -2
  4. package/.agents/agents/session-advisor.md +1 -1
  5. package/.agents/skills/cert-classification-rule/SKILL.md +3 -3
  6. package/.agents/skills/e-code-review/SKILL.md +2 -2
  7. package/.agents/skills/e-code-review/reference/fix.md +1 -1
  8. package/.agents/skills/e-code-review/reference/loop.md +1 -1
  9. package/.agents/skills/e-code-review/reference/medium.md +4 -4
  10. package/.agents/skills/e-code-review/reference/xhigh.md +5 -5
  11. package/.agents/skills/everything-search/SKILL.md +3 -3
  12. package/.agents/skills/issue-tracker/SKILL.md +1 -1
  13. package/.agents/skills/orchestrator/SKILL.md +3 -3
  14. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +1 -1
  15. package/.agents/skills/orchestrator/reference/executor-consult-block.md +1 -1
  16. package/.agents/skills/orchestrator-refresh/SKILL.md +2 -2
  17. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +96 -0
  18. package/.agents/skills/plugin-eval-standalone-skill/reference/graders-and-commands.md +108 -0
  19. package/.agents/skills/privacy-hygiene/SKILL.md +5 -6
  20. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +5 -6
  21. package/.agents/skills/repairing-hook-boundaries/SKILL.md +1 -1
  22. package/.agents/skills/syncing-submodules/SKILL.md +1 -1
  23. package/.agents/skills/team-advisor/SKILL.md +9 -7
  24. package/.agents/skills/team-advisor/reference/advisor-docs-review.md +28 -1
  25. package/.agents/skills/usage-pause/SKILL.md +3 -3
  26. package/_shared/advisor/advisor-protocol.md +7 -3
  27. package/_shared/advisor/reference/advisor-block.md +8 -1
  28. package/audit-rubrics/audit-categories.json +0 -4
  29. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  30. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +2 -2
  31. package/audit-rubrics/category_rubrics/category-i-concurrency.md +1 -1
  32. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
  33. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +1 -1
  34. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +5 -5
  35. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +5 -5
  36. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +3 -3
  37. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +3 -3
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +3 -3
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
  42. package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +12 -12
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +4 -4
  46. package/audit-rubrics/prompts/category-i-concurrency.md +6 -6
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +6 -29
  48. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
  49. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +7 -7
  50. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  51. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +3 -3
  52. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +2 -2
  53. package/bin/ever-shipped-skills.mjs +1 -0
  54. package/bin/ever-shipped-skills.test.mjs +4 -0
  55. package/docs/BDD_SCENARIO_QUALITY.md +2 -2
  56. package/docs/CODE_RULES.md +7 -7
  57. package/docs/TEST_QUALITY.md +1 -1
  58. package/docs/nas-ssh-invocation.md +1 -1
  59. package/docs/references/advisor-tool.md +2 -2
  60. package/docs/references/prose-style-enforcement.md +1 -1
  61. package/docs/references/team-advisor-skill.md +2 -2
  62. package/docs/references/weak-executor-advisor.md +1 -1
  63. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  64. package/package.json +1 -1
  65. package/rules/anti-corollary-tests.md +3 -1
  66. package/rules/archiving-agent-config.md +67 -0
  67. package/rules/asd-ste100-language.md +1 -1
  68. package/rules/ci-owns-the-gate.md +15 -0
  69. package/rules/cleanup-temp-files.md +1 -1
  70. package/rules/code-standards.md +2 -2
  71. package/rules/docstring-prose-matches-implementation.md +1 -1
  72. package/rules/explore-thoroughly.md +2 -2
  73. package/rules/failure-blast-radius.md +9 -8
  74. package/rules/git-workflow.md +7 -4
  75. package/rules/long-horizon-autonomy.md +3 -1
  76. package/rules/nas-ssh-invocation.md +1 -1
  77. package/rules/research-mode.md +3 -1
  78. package/rules/retired-hook-prose.md +10 -2
  79. package/rules/testing.md +3 -3
  80. package/rules/verify-before-asking.md +3 -0
  81. package/rules/verify-runtime-state.md +2 -2
  82. package/scripts/active_capability_references.py +74 -19
  83. package/scripts/banned_prose_words.py +159 -0
  84. package/scripts/dev_env_scripts_constants/active_capability_constants.py +12 -10
  85. package/scripts/dev_env_scripts_constants/banned_prose_word_constants.py +95 -0
  86. package/scripts/policy_lint/adapter_retired_hook_prose.py +17 -7
  87. package/scripts/policy_lint/adapters.py +6 -6
  88. package/scripts/policy_lint/config/constants.py +8 -0
  89. package/scripts/policy_lint/registry.py +1 -1
  90. package/scripts/sync_to_cursor/rules.py +1 -1
  91. package/scripts/test_active_capability_references.py +100 -33
  92. package/scripts/tests/test_banned_prose_words.py +103 -0
  93. package/scripts/tests/test_codex_compat_materializer.py +3 -3
  94. package/scripts/tests/test_engine.py +1 -1
  95. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +46 -0
  96. package/scripts/tests/test_rules.py +12 -1
  97. package/system-prompts/software-engineer.xml +7 -7
@@ -13,12 +13,10 @@ from active_capability_references import (
13
13
  build_capability_inventory,
14
14
  classify_capability_reference,
15
15
  extract_active_capability_names,
16
+ retired_capability_names,
16
17
  strip_inert_fenced_blocks,
17
18
  unresolved_active_capabilities,
18
19
  )
19
- from dev_env_scripts_constants.active_capability_constants import (
20
- ALL_BANNED_ACTIVE_CAPABILITY_NAMES,
21
- )
22
20
 
23
21
  PACKAGE_ROOT = Path(__file__).resolve().parent.parent
24
22
 
@@ -43,66 +41,135 @@ def test_strip_inert_fenced_blocks_drops_historical_examples() -> None:
43
41
  assert "/sr-loop" in stripped
44
42
 
45
43
 
46
- def test_extract_active_capability_names_finds_slash_and_backticks() -> None:
47
- markdown = "Run /privacy-hygiene then open `issue-tracker`.\n"
44
+ def test_extract_active_capability_names_finds_invocations_and_manifests() -> None:
45
+ markdown = (
46
+ "Run /privacy-hygiene then open `issue-tracker/SKILL.md`.\n"
47
+ "See the guide at [review guide](../reviews/SKILL.md).\n"
48
+ )
48
49
  all_hits = extract_active_capability_names(markdown)
49
50
  all_names = {each_name for _line, each_name in all_hits}
50
- assert "privacy-hygiene" in all_names
51
- assert "issue-tracker" in all_names
51
+ assert all_names == {"privacy-hygiene", "issue-tracker", "reviews"}
52
+
53
+
54
+ def test_urls_and_deep_paths_name_no_capability() -> None:
55
+ markdown = (
56
+ "Read [the advisor guide](https://code.claude.com/docs/en/advisor).\n"
57
+ "Call `POST /repos/{owner}/{repo}/issues/{number}/comments`.\n"
58
+ "The default role is `bugteam`.\n"
59
+ )
60
+ assert extract_active_capability_names(markdown) == []
61
+
62
+
63
+ def test_retired_names_come_from_the_shipped_inventory() -> None:
64
+ inventory = build_capability_inventory(PACKAGE_ROOT)
65
+ all_retired_names = retired_capability_names(PACKAGE_ROOT, inventory)
66
+ assert "qbug" in all_retired_names
67
+ assert "findbugs" in all_retired_names
68
+ assert not all_retired_names.intersection(inventory.all_known_names())
52
69
 
53
70
 
54
- def test_banned_names_always_fail_classification() -> None:
55
- for each_banned_name in sorted(ALL_BANNED_ACTIVE_CAPABILITY_NAMES):
56
- reason = classify_capability_reference(each_banned_name)
71
+ def test_a_retired_name_fails_classification() -> None:
72
+ inventory = build_capability_inventory(PACKAGE_ROOT)
73
+ all_retired_names = retired_capability_names(PACKAGE_ROOT, inventory)
74
+ for each_retired_name in sorted(all_retired_names):
75
+ reason = classify_capability_reference(each_retired_name, all_retired_names)
57
76
  assert reason is not None
58
- assert each_banned_name in reason
77
+ assert each_retired_name in reason
59
78
 
60
79
 
61
- def test_known_and_unknown_non_banned_names_pass() -> None:
80
+ def test_shipped_and_unknown_names_pass() -> None:
62
81
  inventory = build_capability_inventory(PACKAGE_ROOT)
63
- known_name = next(iter(inventory.all_skill_names), "e-code-review")
64
- assert classify_capability_reference(known_name) is None
65
- assert classify_capability_reference("not-a-real-capability") is None
82
+ all_retired_names = retired_capability_names(PACKAGE_ROOT, inventory)
83
+ for each_shipped_name in sorted(inventory.all_known_names()):
84
+ assert (
85
+ classify_capability_reference(each_shipped_name, all_retired_names) is None
86
+ ), each_shipped_name
87
+ assert classify_capability_reference("not-a-capability", all_retired_names) is None
66
88
 
67
89
 
68
- def test_unresolved_reports_file_and_line_for_fixture(tmp_path: Path) -> None:
90
+ def test_a_skill_dropped_from_the_tree_reports_without_a_hand_kept_list(
91
+ tmp_path: Path,
92
+ ) -> None:
69
93
  package_root = tmp_path / "pkg"
70
- skills = package_root / "skills" / "real-skill"
71
- skills.mkdir(parents=True)
72
- (skills / "SKILL.md").write_text(
73
- "Active instruction: call /qbug now.\n",
94
+ skill_directory = package_root / "skills" / "kept-skill"
95
+ skill_directory.mkdir(parents=True)
96
+ (skill_directory / "SKILL.md").write_text(
97
+ "Run /dropped-skill after /kept-skill.\n", encoding="utf-8"
98
+ )
99
+ (package_root / "agents").mkdir()
100
+ (package_root / "commands").mkdir()
101
+ (package_root / "bin").mkdir()
102
+ (package_root / "bin" / "ever-shipped-skills.mjs").write_text(
103
+ "export const EVER_SHIPPED_SKILL_NAMES = new Set([\n"
104
+ " 'kept-skill',\n"
105
+ " 'dropped-skill',\n"
106
+ "]);\n",
74
107
  encoding="utf-8",
75
108
  )
109
+ all_unresolved = unresolved_active_capabilities(
110
+ package_root,
111
+ all_relative_markdown_paths=["skills/kept-skill/SKILL.md"],
112
+ )
113
+ assert [each.capability_name for each in all_unresolved] == ["dropped-skill"]
114
+
115
+
116
+ def _write_fixture_package(package_root: Path, skill_name: str, prose: str) -> None:
117
+ skill_directory = package_root / "skills" / skill_name
118
+ skill_directory.mkdir(parents=True)
119
+ (skill_directory / "SKILL.md").write_text(prose, encoding="utf-8")
76
120
  (package_root / "agents").mkdir()
77
- (package_root / "agents" / "real-agent.md").write_text("# agent\n", encoding="utf-8")
121
+ (package_root / "agents" / "shipped-agent.md").write_text(
122
+ "# agent\n", encoding="utf-8"
123
+ )
78
124
  (package_root / "commands").mkdir()
79
- (package_root / "commands" / "real-cmd.md").write_text("# cmd\n", encoding="utf-8")
125
+ (package_root / "commands" / "shipped-command.md").write_text(
126
+ "# command\n", encoding="utf-8"
127
+ )
128
+ (package_root / "bin").mkdir()
129
+ (package_root / "bin" / "ever-shipped-skills.mjs").write_text(
130
+ "export const EVER_SHIPPED_SKILL_NAMES = new Set([\n"
131
+ f" '{skill_name}',\n"
132
+ " 'qbug',\n"
133
+ "]);\n",
134
+ encoding="utf-8",
135
+ )
136
+
137
+
138
+ def test_unresolved_reports_file_and_line_for_fixture(tmp_path: Path) -> None:
139
+ package_root = tmp_path / "pkg"
140
+ _write_fixture_package(
141
+ package_root, "shipped-skill", "Active instruction: call /qbug now.\n"
142
+ )
80
143
  all_unresolved = unresolved_active_capabilities(
81
144
  package_root,
82
- all_relative_markdown_paths=["skills/real-skill/SKILL.md"],
145
+ all_relative_markdown_paths=["skills/shipped-skill/SKILL.md"],
83
146
  )
84
147
  assert all_unresolved
85
148
  first = all_unresolved[0]
86
149
  assert first.capability_name == "qbug"
87
150
  assert first.line_number == 1
88
- assert first.file_path == "skills/real-skill/SKILL.md"
89
- assert "banned" in first.reason
151
+ assert first.file_path == "skills/shipped-skill/SKILL.md"
152
+ assert "retired" in first.reason
90
153
 
91
154
 
92
155
  def test_inert_historical_qbug_does_not_fail(tmp_path: Path) -> None:
93
156
  package_root = tmp_path / "pkg"
94
- skill_dir = package_root / "skills" / "ok-skill"
95
- skill_dir.mkdir(parents=True)
96
- (skill_dir / "SKILL.md").write_text(
157
+ _write_fixture_package(
158
+ package_root,
159
+ "ok-skill",
97
160
  "```historical\nLegacy /qbug workflow.\n```\nUse /ok-skill.\n",
98
- encoding="utf-8",
99
161
  )
100
- (package_root / "agents").mkdir()
101
- (package_root / "commands").mkdir()
102
162
  all_unresolved = unresolved_active_capabilities(
103
163
  package_root,
104
164
  all_relative_markdown_paths=["skills/ok-skill/SKILL.md"],
105
165
  )
106
- assert all(
107
- each.capability_name != "qbug" for each in all_unresolved
166
+ assert all(each.capability_name != "qbug" for each in all_unresolved)
167
+
168
+
169
+ def test_the_package_names_no_retired_capability() -> None:
170
+ all_unresolved = unresolved_active_capabilities(PACKAGE_ROOT)
171
+ report = "\n".join(
172
+ f"{each.file_path}:{each.line_number} {each.capability_name}"
173
+ for each in all_unresolved
108
174
  )
175
+ assert not all_unresolved, report
@@ -0,0 +1,103 @@
1
+ """Behavioral tests for the banned emphasis word detector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from banned_prose_words import (
8
+ banned_prose_words_in_tree,
9
+ find_banned_prose_words,
10
+ governed_paths,
11
+ governs_path,
12
+ prose_lines,
13
+ )
14
+
15
+
16
+ def test_prose_occurrence_is_reported_with_its_line() -> None:
17
+ all_hits = find_banned_prose_words("First line.\nThe real cause is a timeout.\n")
18
+ assert [(each_line, each_word) for each_line, _column, each_word in all_hits] == [
19
+ (2, "real")
20
+ ]
21
+
22
+
23
+ def test_every_banned_form_is_reported() -> None:
24
+ document_text = (
25
+ "It is real.\n"
26
+ "It is really late.\n"
27
+ "A real-world case.\n"
28
+ "The actual value.\n"
29
+ "It actually ran.\n"
30
+ "A genuine failure.\n"
31
+ "It genuinely ran.\n"
32
+ "It truly ran.\n"
33
+ "The true cause.\n"
34
+ )
35
+ all_words = {each_word for _line, _column, each_word in
36
+ find_banned_prose_words(document_text)}
37
+ assert all_words == {
38
+ "real",
39
+ "really",
40
+ "real-world",
41
+ "actual",
42
+ "actually",
43
+ "genuine",
44
+ "genuinely",
45
+ "truly",
46
+ "true",
47
+ }
48
+
49
+
50
+ def test_code_spans_and_paths_carry_no_finding() -> None:
51
+ document_text = (
52
+ "Read `actual_value` from the map.\n"
53
+ "```python\n"
54
+ "actual = compute()\n"
55
+ "```\n"
56
+ " while True:\n"
57
+ "See docs/really-long-name.md for the table.\n"
58
+ "The flag reads always_apply: true.\n"
59
+ )
60
+ assert find_banned_prose_words(document_text) == []
61
+
62
+
63
+ def test_predicative_true_carries_no_finding() -> None:
64
+ document_text = (
65
+ "The condition is true when the port answers.\n"
66
+ "A branch that is always true never runs the other arm.\n"
67
+ )
68
+ assert find_banned_prose_words(document_text) == []
69
+
70
+
71
+ def test_prose_lines_blank_code_and_keep_the_line_count() -> None:
72
+ document_text = "Prose here.\n```python\nactual = 1\n```\nMore prose.\n"
73
+ assert prose_lines(document_text) == ["Prose here.", "", "", "", "More prose."]
74
+
75
+
76
+ def test_governed_paths_lists_the_shipped_surfaces(tmp_path: Path) -> None:
77
+ (tmp_path / "rules").mkdir()
78
+ (tmp_path / "rules" / "one.md").write_text("Prose.\n", encoding="utf-8")
79
+ (tmp_path / "rules-archived").mkdir()
80
+ (tmp_path / "rules-archived" / "two.md").write_text("Prose.\n", encoding="utf-8")
81
+ (tmp_path / "scripts").mkdir()
82
+ (tmp_path / "scripts" / "three.py").write_text("x = 1\n", encoding="utf-8")
83
+ assert governed_paths(tmp_path) == ["rules/one.md"]
84
+
85
+
86
+ def test_governed_paths_cover_instruction_surfaces_and_skip_archives() -> None:
87
+ assert governs_path("rules/git-workflow.md")
88
+ assert governs_path(".agents/skills/eli5/SKILL.md")
89
+ assert governs_path("audit-rubrics/prompts/category-e-dead-code.md")
90
+ assert governs_path("system-prompts/software-engineer.xml")
91
+ assert not governs_path("rules-archived/manifest.md")
92
+ assert not governs_path("scripts/banned_prose_words.py")
93
+ assert not governs_path("CHANGELOG.md")
94
+
95
+
96
+ def test_shipped_instruction_surfaces_carry_no_banned_word() -> None:
97
+ package_root = Path(__file__).resolve().parent.parent.parent
98
+ all_hits = banned_prose_words_in_tree(package_root)
99
+ report = "\n".join(
100
+ f"{each_path}:{each_line}:{each_column} {each_word}"
101
+ for each_path, each_line, each_column, each_word in all_hits[:40]
102
+ )
103
+ assert not all_hits, f"{len(all_hits)} banned words on shipped surfaces:\n{report}"
@@ -826,7 +826,7 @@ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
826
826
  projected_instruction = materializer.render_codex_failure_blast_radius(canonical_rule)
827
827
 
828
828
  assert projected_instruction.startswith("Failure handling for this run")
829
- assert "Three real attempts, then park." in projected_instruction
829
+ assert "Three attempts, then park." in projected_instruction
830
830
  assert "Report as: N of M complete" in projected_instruction
831
831
 
832
832
 
@@ -851,7 +851,7 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
851
851
  assert manifest_record["source"] == "rules/failure-blast-radius.md"
852
852
 
853
853
  rule_path.write_text(
854
- rule_path.read_text(encoding="utf-8").replace("Three real attempts, then park.", "Three tested attempts, then park."),
854
+ rule_path.read_text(encoding="utf-8").replace("Three attempts, then park.", "Three tested attempts, then park."),
855
855
  encoding="utf-8",
856
856
  )
857
857
  drifted_plan, _ = build_plan(config, all_agents=[])
@@ -1021,7 +1021,7 @@ def _assert_projection_and_manifest(
1021
1021
  ) -> None:
1022
1022
  assert not retired_owned_path.exists()
1023
1023
  projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
1024
- assert "Three real attempts, then park." in projected_instruction
1024
+ assert "Three attempts, then park." in projected_instruction
1025
1025
  manifest = json.loads((target / "hooks.json").read_text(encoding="utf-8"))
1026
1026
  assert manifest["hooks"]["PreToolUse"][0]["matcher"] == "apply_patch"
1027
1027
  assert manifest["hooks"]["PreToolUse"][1] == existing_hooks["hooks"]["PreToolUse"][1]
@@ -16,7 +16,7 @@ from sync_to_cursor.engine import run as run_sync_to_cursor
16
16
  _CODE_STANDARDS_SECTION_ORDER = (
17
17
  "COMMENT PRESERVATION",
18
18
  "CORE PRINCIPLES",
19
- "⚡ HOOK-ENFORCED RULES",
19
+ "⚡ LINT-ENFORCED RULES",
20
20
  "3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
21
21
  "5. NO ABBREVIATIONS",
22
22
  "6. COMPLETE TYPE HINTS",
@@ -195,3 +195,49 @@ def test_the_committed_rules_directory_names_no_gate_that_stopped_running() -> N
195
195
  )
196
196
  )
197
197
  assert all_messages == []
198
+
199
+
200
+ def _diagnostics_for_path(
201
+ repository_root: Path, document_path: str, prose: str
202
+ ) -> tuple[str, ...]:
203
+ document = Document(
204
+ PurePosixPath(document_path),
205
+ prose,
206
+ None,
207
+ None,
208
+ ContentOrigin.WORKTREE,
209
+ )
210
+ all_diagnostics = adapter_retired_hook_prose.retired_hook_prose_diagnostics(
211
+ document, repository_root
212
+ )
213
+ return tuple(each_diagnostic.message for each_diagnostic in all_diagnostics)
214
+
215
+
216
+ def test_instruction_surfaces_outside_rules_are_in_scope() -> None:
217
+ for each_path in (
218
+ "packages/claude-dev-env/rules/example.md",
219
+ "packages/claude-dev-env/.agents/skills/example/SKILL.md",
220
+ "packages/claude-dev-env/.agents/agents/example.md",
221
+ "packages/claude-dev-env/commands/example.md",
222
+ "packages/claude-dev-env/docs/example.md",
223
+ ):
224
+ document = Document(
225
+ PurePosixPath(each_path), "", None, None, ContentOrigin.WORKTREE
226
+ )
227
+ assert adapter_retired_hook_prose.accepts_instruction_markdown(document), each_path
228
+
229
+
230
+ def test_a_skill_claim_for_a_surviving_unregistered_hook_is_reported(
231
+ tmp_path: Path,
232
+ ) -> None:
233
+ _build_package(tmp_path)
234
+ (tmp_path / "packages" / "claude-dev-env" / ".agents" / "skills" / "example").mkdir(
235
+ parents=True
236
+ )
237
+ all_messages = _diagnostics_for_path(
238
+ tmp_path,
239
+ "packages/claude-dev-env/.agents/skills/example/SKILL.md",
240
+ "The `windows_rmtree_blocker.py` PreToolUse hook blocks the unsafe pattern.\n",
241
+ )
242
+ assert len(all_messages) == 1
243
+ assert "windows_rmtree_blocker" in all_messages[0]
@@ -9,7 +9,7 @@ _SCRIPTS_DIR = Path(__file__).resolve().parent.parent
9
9
  if str(_SCRIPTS_DIR) not in sys.path:
10
10
  sys.path.insert(0, str(_SCRIPTS_DIR))
11
11
 
12
- from sync_to_cursor.rules import build_mappings
12
+ from sync_to_cursor.rules import build_mappings, merge_code_standards
13
13
 
14
14
  _PACKAGE_ROOT = _SCRIPTS_DIR.parent
15
15
  _SKIPPED_RULE_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md"})
@@ -225,3 +225,14 @@ def test_comment_policy_removes_directive_justification_guidance() -> None:
225
225
  assert "without clear justification" not in react_patterns
226
226
  assert "changed `# noqa` directives are removed or resolved" in category_e_prompt
227
227
  assert "every `# noqa` on an import line must be justified" not in category_e_prompt
228
+
229
+
230
+ def test_merge_code_standards_resolves_every_heading_it_names() -> None:
231
+ merged_text = merge_code_standards(
232
+ (
233
+ _PACKAGE_ROOT / "rules" / "code-standards.md",
234
+ _PACKAGE_ROOT / "docs" / "CODE_RULES.md",
235
+ )
236
+ )
237
+ assert "## Reference (full text:" in merged_text
238
+ assert "## COMMENT PRESERVATION" in merged_text
@@ -72,7 +72,7 @@
72
72
  inferring are replaced by honest acknowledgment. Second, every recommendation, claim, or piece of advice cites a
73
73
  specific source: a file in the current project, an external URL found via web search, a
74
74
  named expert or paper, or official documentation. Third, when working from documents, extract
75
- the actual text first before analyzing — ground responses in direct quotes, and reference
75
+ the text first before analyzing — ground responses in direct quotes, and reference
76
76
  the quote when making a point.
77
77
 
78
78
  Creative thinking, brainstorming, and novel ideas are exempt from citation requirements;
@@ -85,7 +85,7 @@
85
85
  error message, verify the assumption that produced it, and apply a targeted fix. Commit to
86
86
  a viable approach and see it through; switch approaches only when new information directly
87
87
  contradicts the original reasoning. Escalate to the user only after investigation is
88
- complete and a genuine decision point requires their input.
88
+ complete and a decision point requires their input.
89
89
 
90
90
  Treat OWASP top 10 vulnerability classes — command injection, XSS, SQL injection, and
91
91
  related categories — as first-class design criteria. Build code that is inherently resistant
@@ -115,7 +115,7 @@
115
115
  Types — complete coverage required:
116
116
  - Every parameter and return value carries an explicit type hint
117
117
  - Replace Any with the most specific type available
118
- - Avoid `# type: ignore`; remove it and use a typed boundary or real type (AGENTS Types)
118
+ - Avoid `# type: ignore`; remove it and use a typed boundary or an explicit type (AGENTS Types)
119
119
 
120
120
  Configuration — one source of truth:
121
121
  - Every constant lives in config/ only
@@ -156,7 +156,7 @@
156
156
  desired behaviour. Surface assumptions early so they can be confirmed or corrected.
157
157
 
158
158
  Illustrate phase. Explore goals, constraints, and concrete examples with the user. Use
159
- real scenarios — "given X, when Y happens, the system should Z" — to ground the conversation
159
+ scenarios — "given X, when Y happens, the system should Z" — to ground the conversation
160
160
  in observable outcomes.
161
161
 
162
162
  Formulate phase. Express behaviour as developer-facing executable specifications using the
@@ -193,7 +193,7 @@
193
193
 
194
194
  Right-sized engineering: functions over classes, concrete over abstract, YAGNI on API
195
195
  surface. Abstract base classes for single implementations, dependency injection frameworks,
196
- CQRS, microservices, and multiple inheritance hierarchies belong only when a real second
196
+ CQRS, microservices, and multiple inheritance hierarchies belong only when a second
197
197
  use case has arrived.
198
198
  </scope_discipline>
199
199
 
@@ -224,7 +224,7 @@
224
224
  When multiple tool calls are mutually independent, make all calls in a single response.
225
225
  Sequence calls only when a later call requires an earlier call's result. Reading three files:
226
226
  call all three Read operations at once. Running independent searches: launch all Grep
227
- and Glob calls simultaneously. Use real parameter values only. When uncertain whether calls are
227
+ and Glob calls simultaneously. Use concrete parameter values only. When uncertain whether calls are
228
228
  independent, run them sequentially.
229
229
  </agent_workflow>
230
230
 
@@ -363,7 +363,7 @@
363
363
  Prefer working in memory over creating scratchpad files. Use variables and tool results
364
364
  over writing intermediate data to disk.
365
365
 
366
- When a temporary file is genuinely needed — a helper script, a test fixture, a debug output
366
+ When a temporary file is needed — a helper script, a test fixture, a debug output
367
367
  — track it for cleanup. After each task completes, remove every temporary file, script, or
368
368
  helper file created during the task. Leave the working directory cleaner than it was found.
369
369