claude-dev-env 2.31.1 → 2.33.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/.agents/agents/clean-coder.md +8 -7
  2. package/.agents/agents/code-quality-agent.md +4 -2
  3. package/.agents/agents/pr-description-writer.md +8 -3
  4. package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
  5. package/.agents/agents/test_agent_frontmatter.py +0 -9
  6. package/.agents/agents/test_pr_description_writer_style.py +24 -1
  7. package/.agents/skills/e-simplify/SKILL.md +1 -15
  8. package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
  9. package/.agents/skills/pr-refinement/SKILL.md +2 -2
  10. package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
  11. package/.agents/skills/pr-title-description/SKILL.md +6 -2
  12. package/.agents/skills/review-tier/SKILL.md +1 -1
  13. package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
  14. package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
  15. package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
  16. package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
  17. package/AGENTS.md +83 -0
  18. package/_shared/pr-loop/code-rules-gate.md +5 -2
  19. package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
  22. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
  23. package/audit-rubrics/audit-categories.json +2 -2
  24. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  25. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
  26. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
  27. package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
  28. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
  29. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
  30. package/bin/install.test.mjs +11 -3
  31. package/docs/CODE_RULES.md +8 -9
  32. package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
  33. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
  34. package/hooks/blocking/code_rules_comments.py +118 -59
  35. package/hooks/blocking/code_rules_duplicate_body.py +13 -7
  36. package/hooks/blocking/code_rules_enforcer.py +19 -7
  37. package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
  38. package/hooks/blocking/code_rules_type_escape.py +16 -23
  39. package/hooks/blocking/javascript_comment_scanner.py +99 -0
  40. package/hooks/blocking/shell_substitution_blocker.py +175 -8
  41. package/hooks/blocking/stop_dispatcher.py +6 -9
  42. package/hooks/blocking/tdd_enforcer.py +3 -1
  43. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
  44. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
  45. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
  46. package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
  47. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
  48. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  49. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
  50. package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
  51. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
  52. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
  53. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
  54. package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
  55. package/hooks/blocking/test_state_description_blocker.py +56 -71
  56. package/hooks/blocking/test_tdd_enforcer.py +67 -1
  57. package/hooks/git-hooks/pull_request_handoff.py +1 -1
  58. package/hooks/git-hooks/test_post_commit.py +8 -0
  59. package/hooks/hooks.json +2 -2
  60. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
  61. package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
  62. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
  63. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
  64. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
  65. package/hooks/hooks_constants/state_file_lock.py +76 -0
  66. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
  67. package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
  68. package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
  69. package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
  70. package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
  71. package/hooks/observability/session_file_edit_tracker.py +2 -55
  72. package/hooks/observability/test_failure_recorder.py +219 -0
  73. package/hooks/observability/test_test_failure_recorder.py +173 -0
  74. package/hooks/validators/comment_checks.py +2 -2
  75. package/hooks/validators/config/directory_exemption_constants.py +0 -9
  76. package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
  77. package/hooks/validators/fast_save_validators.py +0 -9
  78. package/hooks/validators/git_checks.py +0 -84
  79. package/hooks/validators/mypy_integration.py +10 -36
  80. package/hooks/validators/project_roots.py +58 -0
  81. package/hooks/validators/run_all_validators.py +89 -42
  82. package/hooks/validators/system_temporary_roots.py +1 -1
  83. package/hooks/validators/test_fast_save_validators.py +2 -8
  84. package/hooks/validators/test_git_checks.py +0 -249
  85. package/hooks/validators/test_run_all_validators.py +22 -0
  86. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
  87. package/hooks/validators/test_run_all_validators_reporting.py +66 -0
  88. package/hooks/validators/test_todo_checks.py +1 -0
  89. package/hooks/validators/todo_checks.py +1 -1
  90. package/package.json +1 -1
  91. package/rules/git-workflow.md +0 -8
  92. package/rules/shell-invocation.md +1 -1
  93. package/rules/testing.md +4 -0
  94. package/scripts/check.ps1 +35 -1
  95. package/scripts/codex_compat_materializer.py +1 -0
  96. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
  97. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
  98. package/scripts/shared_tree_paths.py +59 -25
  99. package/scripts/test_shared_tree_paths.py +30 -0
  100. package/scripts/tests/test_rules.py +111 -0
  101. package/system-prompts/software-engineer.xml +6 -19
  102. package/.agents/skills-archived/split-pr/SKILL.md +0 -85
  103. package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
  104. package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
  105. package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
  106. package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
  107. package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
  108. package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
  109. package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
  110. package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
  111. package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
  112. package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
  113. package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
  114. package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
  115. package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
  116. package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
  117. package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
  118. package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
  119. package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
  120. package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
  121. package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
  122. package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
  123. package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
  124. package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
  125. package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
  126. package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
  127. package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
  128. package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
  129. package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
  130. package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
  131. package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
  132. package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
  133. package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
  134. package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
  135. package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
  136. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
  137. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
  138. package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
  139. package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
@@ -141,3 +141,33 @@ def test_missing_module_returns_unresolved_candidate(tmp_path: Path) -> None:
141
141
  assert not (
142
142
  expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME
143
143
  ).is_file()
144
+
145
+
146
+ def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
147
+ """A caller three levels under the root resolves its own sub-package.
148
+
149
+ This is the shape the pr-loop and codex-review scripts carry, which the
150
+ process-tree wrapper's own anchor of one level cannot reach.
151
+ """
152
+ installed_root = tmp_path / "root"
153
+ module_directory = installed_root / "_shared" / "pr-loop" / "scripts"
154
+ module_directory.mkdir(parents=True)
155
+ module_file = str(module_directory / "run_codex_review.py")
156
+ expected_scripts_directory = (
157
+ installed_root
158
+ / SHARED_PACKAGE_DIRECTORY_NAME
159
+ / PROCESS_TREE_DIRECTORY_NAME
160
+ / SCRIPTS_DIRECTORY_NAME
161
+ )
162
+ expected_scripts_directory.mkdir(parents=True)
163
+ (expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME).write_text(
164
+ "", encoding="utf-8"
165
+ )
166
+ resolved_scripts_directory = shared_tree_paths.resolve_shared_scripts_directory(
167
+ module_file,
168
+ {},
169
+ PROCESS_TREE_DIRECTORY_NAME,
170
+ PROCESS_TREE_KILL_MODULE_FILENAME,
171
+ 3,
172
+ )
173
+ assert resolved_scripts_directory == expected_scripts_directory
@@ -76,3 +76,114 @@ def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
76
76
  if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
77
77
  continue
78
78
  assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
79
+
80
+
81
+ def _comment_policy_phrases() -> tuple[str, ...]:
82
+ return (
83
+ "when a change touches code that an existing comment describes or is attached to",
84
+ "leave comments tied to untouched code unchanged",
85
+ "keep comment cleanup inside the requested task",
86
+ "production and tests follow one rule",
87
+ "changed directive, todo, fixme, hack, xxx, and type-ignore comments are removed rather than added or justified",
88
+ )
89
+
90
+
91
+ def _comment_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
92
+ return (
93
+ package_root.parent.parent / "AGENTS.md",
94
+ package_root / "AGENTS.md",
95
+ package_root / "docs" / "CODE_RULES.md",
96
+ package_root / "system-prompts" / "software-engineer.xml",
97
+ package_root / ".agents" / "agents" / "clean-coder.md",
98
+ package_root / ".agents" / "agents" / "code-quality-agent.md",
99
+ package_root / "_shared" / "pr-loop" / "code-rules-gate.md",
100
+ package_root / "audit-rubrics" / "category_rubrics" / "category-j-code-rules-compliance.md",
101
+ package_root / "audit-rubrics" / "prompts" / "category-j-code-rules-compliance.md",
102
+ package_root / "audit-rubrics" / "category_rubrics" / "category-l-behavior-equivalence.md",
103
+ package_root / "audit-rubrics" / "prompts" / "category-l-behavior-equivalence.md",
104
+ package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
105
+ package_root.parent.parent / ".github" / "copilot-instructions.md",
106
+ )
107
+
108
+
109
+ def _worker_policy_phrases() -> tuple[str, ...]:
110
+ return (
111
+ "do not add code comments.",
112
+ "preserve existing comments.",
113
+ "docstrings remain allowed.",
114
+ )
115
+
116
+
117
+ def _worker_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
118
+ return (
119
+ package_root.parent.parent / "AGENTS.md",
120
+ package_root / "AGENTS.md",
121
+ package_root / "docs" / "CODE_RULES.md",
122
+ package_root / "system-prompts" / "software-engineer.xml",
123
+ package_root / ".agents" / "agents" / "clean-coder.md",
124
+ package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
125
+ package_root.parent.parent / ".github" / "copilot-instructions.md",
126
+ )
127
+
128
+
129
+ def test_comment_guidance_reaches_installed_instruction_surfaces() -> None:
130
+ expected_phrases = _comment_policy_phrases()
131
+ all_surface_paths = _comment_policy_surfaces(_PACKAGE_ROOT)
132
+
133
+ for each_surface_path in all_surface_paths:
134
+ surface_text = each_surface_path.read_text(encoding="utf-8").lower()
135
+ for each_phrase in expected_phrases:
136
+ assert each_phrase in surface_text, each_surface_path
137
+ for each_surface_path in _worker_policy_surfaces(_PACKAGE_ROOT):
138
+ surface_text = each_surface_path.read_text(encoding="utf-8").lower()
139
+ for each_phrase in _worker_policy_phrases():
140
+ assert each_phrase in surface_text, each_surface_path
141
+
142
+
143
+ def _category_l_policy_texts() -> tuple[str, str, str]:
144
+ category_l_rubric = (
145
+ _PACKAGE_ROOT
146
+ / "audit-rubrics"
147
+ / "category_rubrics"
148
+ / "category-l-behavior-equivalence.md"
149
+ )
150
+ category_l_prompt = (
151
+ _PACKAGE_ROOT
152
+ / "audit-rubrics"
153
+ / "prompts"
154
+ / "category-l-behavior-equivalence.md"
155
+ )
156
+ audit_categories = _PACKAGE_ROOT / "audit-rubrics" / "audit-categories.json"
157
+ return tuple(
158
+ each_path.read_text(encoding="utf-8").lower()
159
+ for each_path in (category_l_rubric, category_l_prompt, audit_categories)
160
+ )
161
+
162
+
163
+ def _comment_policy_summary_texts() -> tuple[str, str, str, str]:
164
+ clean_coder = _PACKAGE_ROOT / ".agents" / "agents" / "clean-coder.md"
165
+ category_j = (
166
+ _PACKAGE_ROOT
167
+ / "audit-rubrics"
168
+ / "category_rubrics"
169
+ / "category-j-code-rules-compliance.md"
170
+ )
171
+ code_quality = _PACKAGE_ROOT / ".agents" / "agents" / "code-quality-agent.md"
172
+ gate = _PACKAGE_ROOT / "_shared" / "pr-loop" / "code-rules-gate.md"
173
+ return tuple(
174
+ each_path.read_text(encoding="utf-8").lower()
175
+ for each_path in (clean_coder, category_j, code_quality, gate)
176
+ )
177
+
178
+
179
+ def test_comment_policy_uses_changed_comment_names_and_type_directives() -> None:
180
+ category_l_rubric, category_l_prompt, audit_categories = _category_l_policy_texts()
181
+ clean_coder, category_j, code_quality, gate = _comment_policy_summary_texts()
182
+ assert "| l7 | changed-comment handling" in category_l_rubric
183
+ assert "**l7. changed-comment handling**" in category_l_prompt
184
+ assert '"axis_name": "changed-comment handling"' in audit_categories
185
+ assert "no type-ignore directives" in clean_coder
186
+ assert "no `# type: ignore` directives" in category_j
187
+ assert "production and test code" in code_quality
188
+ assert "comments tied to touched code are removed" in gate
189
+ assert "comments tied to untouched code remain unchanged" in gate
@@ -115,8 +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
- - Prefer a real type over `# type: ignore`; when an ignore is required, add a second
119
- trailing `#` justification of at least five characters (AGENTS Types)
118
+ - Avoid `# type: ignore`; remove it and use a typed boundary or real type (AGENTS Types)
120
119
 
121
120
  Configuration — one source of truth:
122
121
  - Every constant lives in config/ only
@@ -124,10 +123,12 @@
124
123
  - UPPER_SNAKE names belong in config/ only
125
124
 
126
125
  Comments — preservation, add-time blocking:
127
- - Keep every comment on an untouched line untouched; removing one prints a stderr advisory and lets the edit through
128
- - Adding a new inline comment in production code blocks the edit
126
+ - Preserve existing comments.
127
+ - Do not add code comments.
128
+ - When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
129
+ - Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
129
130
  - Express new explanations through self-documenting names
130
- - Docstrings on new files, methods, and classes are permitted
131
+ - Docstrings remain allowed.
131
132
 
132
133
  Style — enforced at write time:
133
134
  - Imports belong at the top of the file
@@ -197,11 +198,6 @@
197
198
  </scope_discipline>
198
199
 
199
200
  <git_workflow>
200
- Maintain one commit per review stage. The initial feature is one commit. Each subsequent
201
- review round adds exactly one commit. Each round's fixes belong in a single, dedicated
202
- commit; keep the initial feature commit and each review-fix commit separate to preserve
203
- the full review history.
204
-
205
201
  Keep working documents and image files out of the repository. Files matching these patterns
206
202
  belong outside the repo:
207
203
  - docs/plans/*.md — working planning documents
@@ -212,13 +208,6 @@
212
208
  When multiple features need shared infrastructure, extract the shared infrastructure into
213
209
  its own PR, get it reviewed and merged, then launch parallel feature PRs that import it.
214
210
 
215
- When running as a GitHub Action (detected via the GITHUB_ACTIONS=true environment variable),
216
- commit and push each logical change as it completes rather than batching the full session
217
- into a single commit at the end. Each incremental commit carries a focused message
218
- describing only that change. This gives PR observers real-time progress and lets them
219
- intervene mid-run. The "one commit per review stage" rule above applies to interactive
220
- local sessions; in the GitHub Actions context the entire run is the review stage, and
221
- incremental commits within it are the expected shape.
222
211
  </git_workflow>
223
212
 
224
213
  <agent_workflow>
@@ -251,8 +240,6 @@
251
240
 
252
241
  Reply to each comment inline on the PR after the fix is in place.
253
242
 
254
- Produce one review-fix commit per review round. Each round gets exactly one commit covering
255
- all fixes for that round.
256
243
  </code_review_response>
257
244
 
258
245
  <documentation>
@@ -1,85 +0,0 @@
1
- ---
2
- name: split-pr
3
- description: >-
4
- Source-owned PR analyzer for the hand-written line metric and 200/600 split
5
- gates. Triggers: /split-pr analyze, split analysis, hand-written lines,
6
- 200/600 PR split.
7
- ---
8
-
9
- # split-pr (analyzer)
10
-
11
- **Source-owned analyzer** for the locked hand-written line metric and the 200 /
12
- 600 split gates. Reports hand-written churn separately from excluded churn
13
- (generated, vendor, minified, lockfile). File count informs judgment and is
14
- never a hard gate.
15
-
16
- ## Gotchas
17
-
18
- - **Hand-written lines are additions plus deletions** on non-excluded paths.
19
- - **200 hand-written lines** require a recorded `split_analysis` document.
20
- - **600 hand-written lines** default to multiple PR slices (`default_split`).
21
- - **Atomic exception** at 600+ needs a recorded unsplittable reason and a
22
- standing Fable verdict token — never stay one PR silently.
23
- - **File count is context only.** It never blocks analysis.
24
-
25
- ## When this applies
26
-
27
- - User asks for a split analysis or hand-written line report on a PR or file list.
28
- - A PR approaches or exceeds the 200 / 600 hand-written line surfaces.
29
-
30
- ## Process
31
-
32
- 1. Run `python scripts/analyze_pr.py --files-json <path> --pretty` (or `--pr N`).
33
- 2. Read `hand_written_lines`, `excluded_churn_lines`, `requires_split_analysis`,
34
- `default_split`, and optional `atomic_exception`.
35
- 3. At 200+ without a prior analysis artifact, record the analyzer JSON before
36
- shipping further commits on that surface.
37
- 4. At 600+ with `default_split=true`, plan multiple PR slices unless
38
- `atomic_exception` carries a Fable-approved reason.
39
-
40
- ## File index
41
-
42
- | File | Purpose |
43
- |---|---|
44
- | `SKILL.md` | Analyzer contract and 200/600 gates |
45
- | `scripts/analyze_pr.py` | Emit analysis JSON from gh or offline files |
46
- | `scripts/categorize_files.py` | Path churn class (hand-written vs excluded) |
47
- | `scripts/split_pr_script_types.py` | Canonical split-plan build and path-assignment validation |
48
- | `scripts/split_pr_title.py` | Collapse titles to exactly one conventional prefix |
49
- | `scripts/split_pr_layer_order.py` | Deterministic config→other layer sort |
50
- | `scripts/split_pr_paginate.py` | Paginated PR file intake via gh api --slurp |
51
- | `scripts/pack_files_into_slices.py` | Pack annotated files into ≤200 hand-written-line slices by layer |
52
- | `scripts/config/packing_constants.py` | Review budget, hard cap, path→layer markers for packing |
53
- | `scripts/test_categorize_files_packing.py` | Budget packing and layer inference tests |
54
- | `scripts/verify_plan.py` | Coverage verification: unique path assignment, path normalization, title contract |
55
- | `scripts/test_verify_plan.py` | Full-coverage and gap rejection tests |
56
- | `scripts/test_verify_plan_contract.py` | source_commit and title-normalization contract tests |
57
- | `scripts/test_verify_plan_path_normalization.py` | Unsafe-path fail-closed tests |
58
- | `scripts/split_pr_dependency_graph.py` | Deterministic layer-rank dependency graph over slices |
59
- | `scripts/config/dependency_constants.py` | Graph key names and layer ranks |
60
- | `scripts/test_split_pr_dependency_graph.py` | Graph order, duplicate-id, unknown-layer tests |
61
- | `scripts/verify_dependency_graph.py` | CLI + shape/order checks for a dependency graph document |
62
- | `scripts/test_verify_dependency_graph.py` | Rebuild-order accept/reject and CLI exit tests |
63
- | `scripts/config/split_pr_constants.py` | Thresholds and excluded path markers |
64
- | `scripts/config/plan_constants.py` | Plan schema keys, layer order, title-prefix tokens |
65
- | `scripts/test_analyze_pr.py` | Boundary tests at 199/200/599/600 |
66
- | `scripts/test_categorize_files.py` | Exclusion classification tests |
67
- | `scripts/test_split_pr_script_types.py` | Plan schema and full path assignment |
68
- | `scripts/test_split_pr_title.py` | One-prefix title normalization |
69
- | `scripts/test_split_pr_layer_order.py` | Layer order ranking |
70
- | `scripts/test_split_pr_paginate.py` | Slurp page flattening |
71
- | `scripts/split_pr_process_runner.py` | Captured subprocess runner for Git ops |
72
- | `scripts/split_pr_git_operations.py` | Safe local slice commit + restore via git_operations_constants |
73
- | `scripts/execute_split_slices.py` | Materialize full plan in topo order |
74
- | `scripts/config/git_operations_constants.py` | Git argv tokens and error strings |
75
- | `scripts/test_split_pr_git_operations.py` | Dirty-tree, path staging, restore tests |
76
- | `scripts/test_execute_split_slices.py` | Plan walk order and failure restore |
77
- | `reference/proposal-format.md` | Human-readable plan field contract |
78
- | `reference/splitting-principles.md` | Vertical-slice invariants |
79
- | `reference/path-layers.md` | Layer hints (context only) |
80
- | `reference/split-further-loop.md` | Re-split loop until leaves fit review |
81
-
82
- ## Folder map
83
-
84
- - `scripts/` — analyzer, categorize helpers, plan verifier, plan contract, constants, tests
85
- - `reference/` — vertical-slice principles, layer hints, plan field contract
@@ -1,16 +0,0 @@
1
- # Path layers — context only
2
-
3
- Path-layer labels (database, contracts, backend, frontend, tests, config) are
4
- **judgment aids**, not hard gates. Vertical slice assignment overrides layer
5
- buckets when tests must travel with the modules they cover.
6
-
7
- | Layer hint | Typical paths |
8
- |---|---|
9
- | database | `**/migrations/**`, `**/schema.prisma` |
10
- | contracts | `**/api/**/types*`, OpenAPI specs |
11
- | backend | `**/services/**`, `**/handlers/**` |
12
- | frontend | `**/components/**`, `**/*.tsx` |
13
- | tests | `**/test_*.py`, `**/*_test.py`, `**/*.test.*` |
14
- | config | lockfiles, `package.json`, CI workflows |
15
-
16
- File count and layer count inform review; they never alone block a plan.
@@ -1,15 +0,0 @@
1
- # Split-plan proposal format
2
-
3
- Machine-readable plan documents use `schema_version` `1` with:
4
-
5
- | Field | Meaning |
6
- |---|---|
7
- | `source_commit` | Exact SHA the changed-file list was taken from |
8
- | `all_changed_paths` | Full path set for that commit |
9
- | `all_slices` | Ordered slices after layer sort |
10
-
11
- Each slice carries `id`, `title` (exactly one conventional prefix), `layer`
12
- (one of `config` / `backend` / `frontend` / `tests` / `docs` / `other`), and
13
- `all_paths`. Every path in `all_changed_paths` is assigned to exactly one
14
- slice. Titles are normalized with `split_pr_title.normalize_split_title`.
15
- Layer tokens follow `ALL_LAYER_ORDER` in `packages/claude-dev-env/.agents/skills-archived/split-pr/scripts/config/plan_constants.py`.
@@ -1,10 +0,0 @@
1
- # Split-further loop
2
-
3
- After the first vertical plan lands as draft slices, re-check each leaf PR:
4
-
5
- 1. Run the hand-written line analyzer (`scripts/analyze_pr.py`).
6
- 2. If hand-written lines ≥ 200, record a split-analysis artifact.
7
- 3. If hand-written lines ≥ 600 and no atomic exception, propose a further
8
- vertical split (tests still co-located with behavior).
9
- 4. Stop when every leaf fits review or carries a Fable-approved atomic
10
- exception.
@@ -1,26 +0,0 @@
1
- # Splitting principles — vertical slices
2
-
3
- A split plan groups changed paths into **semantic vertical slices**, not layer
4
- silos. Each slice tells one reviewable story and keeps related tests beside the
5
- behavior they cover.
6
-
7
- ## Invariants
8
-
9
- 1. **Test co-location.** A behavior change and its related tests share one
10
- slice. Tests do not land in a later dedicated-test slice by default.
11
- 2. **One story per slice.** Each slice has a single review focus (one vertical
12
- capability or one coherent subsystem story).
13
- 3. **Unique path ownership.** Every changed path appears in exactly one slice.
14
- 4. **Green intermediate.** Each stacked intermediate must collect and run the
15
- tests that belong to that slice and its bases.
16
- 5. **Preparatory refactors first.** Substantial refactors that do not change
17
- product behavior occupy earlier preparatory slices, not mixed with behavior.
18
- 6. **Dependency bases encode merge order.** Slice N+1 bases on slice N when it
19
- depends on N's definitions.
20
-
21
- ## Fable gates
22
-
23
- Standing Fable approval is required for:
24
-
25
- - a semantic re-bucket that moves paths across slices after the first plan
26
- - an oversized atomic exception that keeps an unsplittable surface as one PR
@@ -1,279 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Analyze PR churn with the hand-written line metric and 200/600 gates.
3
-
4
- ::
5
-
6
- python analyze_pr.py --files-json files.json --pretty
7
- {"hand_written_lines": 200, "requires_split_analysis": true, ...}
8
-
9
- Uses paginated ``gh api .../pulls/N/files`` unless ``--files-json`` is offline.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import argparse
15
- import json
16
- import subprocess
17
- import sys
18
- from pathlib import Path
19
-
20
- from categorize_files import annotate_files, sum_churn_by_class
21
- from config.split_pr_constants import (
22
- CHURN_CLASS_HAND_WRITTEN,
23
- DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD,
24
- EXIT_CODE_FAILURE,
25
- EXIT_CODE_SUCCESS,
26
- FILE_KEY_ADDITIONS,
27
- FILE_KEY_CHANGED_LINES,
28
- FILE_KEY_CHURN_CLASS,
29
- FILE_KEY_DELETIONS,
30
- FILE_KEY_PATH,
31
- GH_API_SUBCOMMAND,
32
- GH_COMMAND,
33
- GH_FILE_ADDITIONS,
34
- GH_FILE_DELETIONS,
35
- GH_FILE_FILENAME,
36
- GH_FILE_PATH,
37
- GH_JSON_FLAG,
38
- GH_NAME_WITH_OWNER_FIELD,
39
- GH_PAGINATE_FLAG,
40
- GH_PULLS_FILES_PATH_TEMPLATE,
41
- GH_REPO_SUBCOMMAND,
42
- GH_SLURP_FLAG,
43
- GH_VIEW,
44
- JSON_INDENT_SPACES,
45
- PAYLOAD_KEY_ALL_FILES,
46
- PAYLOAD_KEY_ATOMIC_EXCEPTION,
47
- PAYLOAD_KEY_DEFAULT_SPLIT,
48
- PAYLOAD_KEY_ERROR,
49
- PAYLOAD_KEY_EXCLUDED_CHURN_LINES,
50
- PAYLOAD_KEY_FABLE_VERDICT,
51
- PAYLOAD_KEY_FILE_COUNT,
52
- PAYLOAD_KEY_HAND_WRITTEN_LINES,
53
- PAYLOAD_KEY_REASON,
54
- PAYLOAD_KEY_REQUIRES_SPLIT_ANALYSIS,
55
- SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD,
56
- UTF8_ENCODING,
57
- )
58
-
59
- JsonObject = dict[str, object]
60
-
61
-
62
- def build_analysis_from_files(
63
- all_file_records: list[JsonObject],
64
- *,
65
- atomic_exception_reason: str | None = None,
66
- fable_verdict: str | None = None,
67
- ) -> JsonObject:
68
- """Build the hand-written line analysis document.
69
-
70
- Args:
71
- all_file_records: Raw path/additions/deletions maps.
72
- atomic_exception_reason: When set at 600+, records an atomic exception.
73
- fable_verdict: Standing Fable verdict token required with the exception.
74
-
75
- Returns:
76
- Analysis payload with thresholds and optional atomic_exception.
77
-
78
- Raises:
79
- ValueError: When an atomic exception is set without a Fable verdict.
80
- """
81
- all_annotated = annotate_files(all_file_records)
82
- hand_written_lines = sum_churn_by_class(
83
- all_annotated, churn_class=CHURN_CLASS_HAND_WRITTEN
84
- )
85
- excluded_churn_lines = 0
86
- for each_record in all_annotated:
87
- if each_record.get(FILE_KEY_CHURN_CLASS) != CHURN_CLASS_HAND_WRITTEN:
88
- excluded_churn_lines += int(
89
- each_record.get(FILE_KEY_CHANGED_LINES, 0) or 0
90
- )
91
- file_count = len(all_annotated)
92
- requires_split_analysis = (
93
- hand_written_lines >= SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD
94
- )
95
- default_split = hand_written_lines >= DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD
96
- analysis: JsonObject = {
97
- PAYLOAD_KEY_HAND_WRITTEN_LINES: hand_written_lines,
98
- PAYLOAD_KEY_EXCLUDED_CHURN_LINES: excluded_churn_lines,
99
- PAYLOAD_KEY_FILE_COUNT: file_count,
100
- PAYLOAD_KEY_REQUIRES_SPLIT_ANALYSIS: requires_split_analysis,
101
- PAYLOAD_KEY_DEFAULT_SPLIT: default_split,
102
- PAYLOAD_KEY_ALL_FILES: all_annotated,
103
- PAYLOAD_KEY_ATOMIC_EXCEPTION: None,
104
- }
105
- if default_split and atomic_exception_reason:
106
- if not fable_verdict:
107
- raise ValueError(
108
- "atomic exception at 600+ hand-written lines requires a "
109
- "standing Fable verdict"
110
- )
111
- analysis[PAYLOAD_KEY_ATOMIC_EXCEPTION] = {
112
- PAYLOAD_KEY_REASON: atomic_exception_reason,
113
- PAYLOAD_KEY_FABLE_VERDICT: fable_verdict,
114
- }
115
- analysis[PAYLOAD_KEY_DEFAULT_SPLIT] = False
116
- return analysis
117
-
118
-
119
- def _file_records_from_gh(raw_files: object) -> list[JsonObject]:
120
- if not isinstance(raw_files, list):
121
- return []
122
- all_file_records: list[JsonObject] = []
123
- for each_file in raw_files:
124
- if not isinstance(each_file, dict):
125
- continue
126
- path = each_file.get(GH_FILE_PATH) or each_file.get(GH_FILE_FILENAME)
127
- if not path:
128
- continue
129
- all_file_records.append(
130
- {
131
- FILE_KEY_PATH: str(path),
132
- FILE_KEY_ADDITIONS: int(each_file.get(GH_FILE_ADDITIONS, 0) or 0),
133
- FILE_KEY_DELETIONS: int(each_file.get(GH_FILE_DELETIONS, 0) or 0),
134
- }
135
- )
136
- return all_file_records
137
-
138
-
139
- def _flatten_paginated_file_pages(payload: object) -> list[object]:
140
- if not isinstance(payload, list):
141
- return []
142
- all_files: list[object] = []
143
- for each_page in payload:
144
- if isinstance(each_page, list):
145
- all_files.extend(each_page)
146
- elif isinstance(each_page, dict):
147
- all_files.append(each_page)
148
- return all_files
149
-
150
-
151
- def _load_files_json(files_json_path: Path) -> list[JsonObject]:
152
- payload = json.loads(files_json_path.read_text(encoding=UTF8_ENCODING))
153
- if not isinstance(payload, list):
154
- raise ValueError("files-json must be a JSON array")
155
- return _file_records_from_gh(payload)
156
-
157
-
158
- def _resolve_repo_name(repo: str | None) -> str:
159
- if repo:
160
- return repo
161
- completed = subprocess.run(
162
- [
163
- GH_COMMAND,
164
- GH_REPO_SUBCOMMAND,
165
- GH_VIEW,
166
- GH_JSON_FLAG,
167
- GH_NAME_WITH_OWNER_FIELD,
168
- ],
169
- capture_output=True,
170
- text=True,
171
- check=False,
172
- encoding=UTF8_ENCODING,
173
- )
174
- if completed.returncode != EXIT_CODE_SUCCESS:
175
- raise RuntimeError(
176
- completed.stderr.strip()
177
- or "gh repo view failed; pass --repo owner/name"
178
- )
179
- payload = json.loads(completed.stdout)
180
- name_with_owner = payload.get(GH_NAME_WITH_OWNER_FIELD)
181
- if not name_with_owner:
182
- raise RuntimeError("gh repo view returned no nameWithOwner; pass --repo")
183
- return str(name_with_owner)
184
-
185
-
186
- def _fetch_pr_files(pr_number: int, repo: str | None) -> list[JsonObject]:
187
- resolved_repo = _resolve_repo_name(repo)
188
- api_path = GH_PULLS_FILES_PATH_TEMPLATE.format(
189
- repo=resolved_repo,
190
- pr_number=pr_number,
191
- )
192
- all_command = [
193
- GH_COMMAND,
194
- GH_API_SUBCOMMAND,
195
- api_path,
196
- GH_PAGINATE_FLAG,
197
- GH_SLURP_FLAG,
198
- ]
199
- completed = subprocess.run(
200
- all_command,
201
- capture_output=True,
202
- text=True,
203
- check=False,
204
- encoding=UTF8_ENCODING,
205
- )
206
- if completed.returncode != EXIT_CODE_SUCCESS:
207
- raise RuntimeError(completed.stderr.strip() or "gh api pulls files failed")
208
- payload = json.loads(completed.stdout)
209
- return _file_records_from_gh(_flatten_paginated_file_pages(payload))
210
-
211
-
212
- def _analysis_for_cli(
213
- all_file_records: list[JsonObject],
214
- exception_reason: str | None,
215
- standing_verdict: str | None,
216
- ) -> JsonObject:
217
- """Map CLI flag values onto the analysis builder kwargs."""
218
- return build_analysis_from_files(
219
- all_file_records,
220
- atomic_exception_reason=exception_reason,
221
- fable_verdict=standing_verdict,
222
- )
223
-
224
-
225
- def main() -> int:
226
- """CLI entry: emit analysis JSON to stdout.
227
-
228
- Returns:
229
- Process exit code.
230
-
231
- Raises:
232
- Does not raise; converts analysis failures into a JSON error payload.
233
- """
234
- parser = argparse.ArgumentParser(description="Hand-written line PR analyzer")
235
- parser.add_argument("--pr", type=int, default=0, help="PR number for gh fetch")
236
- parser.add_argument("--repo", default=None, help="owner/name for gh")
237
- parser.add_argument(
238
- "--files-json",
239
- type=Path,
240
- default=None,
241
- help="Offline files payload (tests); skips gh",
242
- )
243
- parser.add_argument(
244
- "--atomic-exception-reason",
245
- default=None,
246
- help="Unsplittable reason when keeping one PR at 600+",
247
- )
248
- parser.add_argument(
249
- "--fable-verdict",
250
- default=None,
251
- help="Standing Fable verdict token for the atomic exception",
252
- )
253
- parser.add_argument("--pretty", action="store_true", help="Indent JSON output")
254
- parsed = parser.parse_args()
255
- try:
256
- if parsed.files_json is not None:
257
- all_file_records = _load_files_json(parsed.files_json)
258
- elif parsed.pr > 0:
259
- all_file_records = _fetch_pr_files(parsed.pr, parsed.repo)
260
- else:
261
- raise ValueError("provide --pr or --files-json")
262
- analysis = _analysis_for_cli(
263
- all_file_records,
264
- parsed.atomic_exception_reason,
265
- parsed.fable_verdict,
266
- )
267
- except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as error:
268
- print(
269
- json.dumps({PAYLOAD_KEY_ERROR: str(error)}),
270
- file=sys.stdout,
271
- )
272
- return EXIT_CODE_FAILURE
273
- indent = JSON_INDENT_SPACES if parsed.pretty else None
274
- print(json.dumps(analysis, indent=indent))
275
- return EXIT_CODE_SUCCESS
276
-
277
-
278
- if __name__ == "__main__":
279
- raise SystemExit(main())