claude-dev-env 1.39.0 → 1.40.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 (27) hide show
  1. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +10 -0
  2. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  3. package/_shared/pr-loop/scripts/post_audit_thread.py +296 -1
  4. package/_shared/pr-loop/scripts/preflight.py +129 -2
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  6. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +194 -1
  7. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  8. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  9. package/agents/pr-description-writer.md +150 -52
  10. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  11. package/hooks/blocking/pr_description_enforcer.py +57 -22
  12. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  13. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  14. package/package.json +1 -1
  15. package/skills/bugteam/SKILL.md +28 -10
  16. package/skills/bugteam/reference/team-setup.md +5 -0
  17. package/skills/bugteam/scripts/bugteam_preflight.py +36 -2
  18. package/skills/bugteam/scripts/test_bugteam_preflight.py +41 -0
  19. package/skills/copilot-review/SKILL.md +16 -0
  20. package/skills/findbugs/SKILL.md +35 -7
  21. package/skills/monitor-open-prs/SKILL.md +2 -1
  22. package/skills/pr-converge/SKILL.md +3 -1
  23. package/skills/pr-converge/config/constants.py +1 -0
  24. package/skills/pr-converge/reference/per-tick.md +17 -0
  25. package/skills/pr-converge/scripts/check_bugbot_ci.py +113 -8
  26. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  27. package/skills/qbug/SKILL.md +33 -8
@@ -43,7 +43,9 @@ if str(SCRIPT_DIRECTORY) not in sys.path:
43
43
  from config.post_audit_thread_constants import ( # noqa: E402
44
44
  ALL_GH_AUTH_TOKEN_COMMAND_PARTS,
45
45
  ALL_RETRY_BACKOFF_SECONDS,
46
+ BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
46
47
  GH_TOKEN_ENV_VAR_NAME,
48
+ GITHUB_TOKEN_ENV_VAR_NAME,
47
49
  CLI_FLAG_COMMIT,
48
50
  CLI_FLAG_FINDINGS_JSON,
49
51
  CLI_FLAG_OWNER,
@@ -69,7 +71,15 @@ from config.post_audit_thread_constants import ( # noqa: E402
69
71
  STATE_CLEAN,
70
72
  STATE_DIRTY,
71
73
  )
72
- from post_audit_thread import build_reviews_endpoint_url # noqa: E402
74
+ from post_audit_thread import ( # noqa: E402
75
+ UserInputError,
76
+ build_reviews_endpoint_url,
77
+ fetch_gh_token_for_account,
78
+ list_authenticated_gh_account_logins,
79
+ query_active_gh_user_login,
80
+ query_pull_request_author_login,
81
+ resolve_reviewer_token,
82
+ )
73
83
 
74
84
  LIVE_TEST_OWNER = "JonEcho"
75
85
  LIVE_TEST_REPO = "tests"
@@ -918,6 +928,189 @@ class LivePostAuditThreadTests(unittest.TestCase):
918
928
  f"(1s + 4s + 16s); elapsed={elapsed_seconds:.2f}s",
919
929
  )
920
930
 
931
+ def _isolate_auth_env_vars(self) -> dict[str, str | None]:
932
+ all_managed_env_var_names = (
933
+ GH_TOKEN_ENV_VAR_NAME,
934
+ GITHUB_TOKEN_ENV_VAR_NAME,
935
+ BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
936
+ )
937
+ previous_env_state: dict[str, str | None] = {
938
+ each_name: os.environ.get(each_name)
939
+ for each_name in all_managed_env_var_names
940
+ }
941
+ for each_name in all_managed_env_var_names:
942
+ os.environ.pop(each_name, None)
943
+ return previous_env_state
944
+
945
+ def _restore_auth_env_vars(
946
+ self, previous_env_state: dict[str, str | None]
947
+ ) -> None:
948
+ for each_name, prior_value in previous_env_state.items():
949
+ if prior_value is None:
950
+ os.environ.pop(each_name, None)
951
+ else:
952
+ os.environ[each_name] = prior_value
953
+
954
+ def test_query_active_gh_user_login_matches_gh_api_user_login_field(self) -> None:
955
+ active_login = query_active_gh_user_login()
956
+ self.assertTrue(
957
+ active_login,
958
+ "query_active_gh_user_login() returned empty",
959
+ )
960
+ gh_api_user_response = gh_api_object_json("user")
961
+ self.assertEqual(active_login, gh_api_user_response.get("login"))
962
+
963
+ def test_query_pull_request_author_login_matches_throwaway_pr_author(self) -> None:
964
+ author_login = query_pull_request_author_login(
965
+ owner=LIVE_TEST_OWNER,
966
+ repo=LIVE_TEST_REPO,
967
+ pr_number=self.pr_number,
968
+ )
969
+ pr_detail_path = f"repos/{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}/pulls/{self.pr_number}"
970
+ pr_detail_object = gh_api_object_json(pr_detail_path)
971
+ user_field_object = pr_detail_object.get("user")
972
+ self.assertIsInstance(user_field_object, dict)
973
+ if isinstance(user_field_object, dict):
974
+ self.assertEqual(author_login, user_field_object.get("login"))
975
+
976
+ def test_list_authenticated_gh_account_logins_includes_active_and_audit_accounts(
977
+ self,
978
+ ) -> None:
979
+ all_logins = list_authenticated_gh_account_logins()
980
+ active_login = query_active_gh_user_login()
981
+ self.assertIn(active_login, all_logins)
982
+ self.assertIn(LIVE_TEST_AUDIT_ACCOUNT_NAME, all_logins)
983
+
984
+ def test_fetch_gh_token_for_account_returns_audit_account_cached_token(self) -> None:
985
+ fetched_token = fetch_gh_token_for_account(LIVE_TEST_AUDIT_ACCOUNT_NAME)
986
+ self.assertEqual(fetched_token, self.audit_account_token)
987
+
988
+ def test_resolve_reviewer_token_returns_env_var_when_gh_token_is_set(self) -> None:
989
+ sentinel_env_token = "sentinel-gh-token-from-env-var-precedence-test"
990
+ previous_env_state = self._isolate_auth_env_vars()
991
+ try:
992
+ os.environ[GH_TOKEN_ENV_VAR_NAME] = sentinel_env_token
993
+ returned_token = resolve_reviewer_token(
994
+ owner=LIVE_TEST_OWNER,
995
+ repo=LIVE_TEST_REPO,
996
+ pr_number=self.pr_number,
997
+ )
998
+ self.assertEqual(returned_token, sentinel_env_token)
999
+ finally:
1000
+ self._restore_auth_env_vars(previous_env_state)
1001
+
1002
+ def test_resolve_reviewer_token_toggles_to_alternate_token_on_self_pr(self) -> None:
1003
+ previous_env_state = self._isolate_auth_env_vars()
1004
+ try:
1005
+ returned_token = resolve_reviewer_token(
1006
+ owner=LIVE_TEST_OWNER,
1007
+ repo=LIVE_TEST_REPO,
1008
+ pr_number=self.pr_number,
1009
+ )
1010
+ active_login = query_active_gh_user_login()
1011
+ pr_author_login = query_pull_request_author_login(
1012
+ owner=LIVE_TEST_OWNER,
1013
+ repo=LIVE_TEST_REPO,
1014
+ pr_number=self.pr_number,
1015
+ )
1016
+ self.assertEqual(
1017
+ active_login.lower(),
1018
+ pr_author_login.lower(),
1019
+ "throwaway PR author must equal active gh account so the "
1020
+ "self-PR toggle branch is exercised",
1021
+ )
1022
+ all_alternates = [
1023
+ each_login
1024
+ for each_login in list_authenticated_gh_account_logins()
1025
+ if each_login.lower() != pr_author_login.lower()
1026
+ ]
1027
+ self.assertTrue(
1028
+ all_alternates,
1029
+ "test setup requires at least one alternate authenticated account",
1030
+ )
1031
+ expected_first_alternate_token = fetch_gh_token_for_account(
1032
+ all_alternates[0]
1033
+ )
1034
+ self.assertEqual(returned_token, expected_first_alternate_token)
1035
+ active_account_token = resolve_gh_auth_token()
1036
+ self.assertNotEqual(
1037
+ returned_token,
1038
+ active_account_token,
1039
+ "self-PR toggle must not return the active (author) token",
1040
+ )
1041
+ finally:
1042
+ self._restore_auth_env_vars(previous_env_state)
1043
+
1044
+ def test_resolve_reviewer_token_honors_bugteam_reviewer_account_pin(self) -> None:
1045
+ previous_env_state = self._isolate_auth_env_vars()
1046
+ try:
1047
+ pr_author_login = query_pull_request_author_login(
1048
+ owner=LIVE_TEST_OWNER,
1049
+ repo=LIVE_TEST_REPO,
1050
+ pr_number=self.pr_number,
1051
+ )
1052
+ all_alternates_excluding_pr_author = [
1053
+ each_login
1054
+ for each_login in list_authenticated_gh_account_logins()
1055
+ if each_login.lower() != pr_author_login.lower()
1056
+ ]
1057
+ self.assertTrue(
1058
+ all_alternates_excluding_pr_author,
1059
+ "test setup requires at least one authenticated account that "
1060
+ "is not the PR author so the pin has a valid target",
1061
+ )
1062
+ chosen_pin_login = all_alternates_excluding_pr_author[0]
1063
+ os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = chosen_pin_login
1064
+ returned_token = resolve_reviewer_token(
1065
+ owner=LIVE_TEST_OWNER,
1066
+ repo=LIVE_TEST_REPO,
1067
+ pr_number=self.pr_number,
1068
+ )
1069
+ expected_pinned_token = fetch_gh_token_for_account(chosen_pin_login)
1070
+ self.assertEqual(returned_token, expected_pinned_token)
1071
+ finally:
1072
+ self._restore_auth_env_vars(previous_env_state)
1073
+
1074
+ def test_resolve_reviewer_token_error_excludes_pr_author_from_candidate_set(
1075
+ self,
1076
+ ) -> None:
1077
+ unauthenticated_account_name = "intentionally-not-authenticated-account-zzz"
1078
+ previous_env_state = self._isolate_auth_env_vars()
1079
+ try:
1080
+ os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = (
1081
+ unauthenticated_account_name
1082
+ )
1083
+ with self.assertRaises(UserInputError) as raised_context:
1084
+ resolve_reviewer_token(
1085
+ owner=LIVE_TEST_OWNER,
1086
+ repo=LIVE_TEST_REPO,
1087
+ pr_number=self.pr_number,
1088
+ )
1089
+ error_message_text = str(raised_context.exception)
1090
+ self.assertIn(unauthenticated_account_name, error_message_text)
1091
+ pr_author_login = query_pull_request_author_login(
1092
+ owner=LIVE_TEST_OWNER,
1093
+ repo=LIVE_TEST_REPO,
1094
+ pr_number=self.pr_number,
1095
+ )
1096
+ all_alternates_at_call_time = [
1097
+ each_login
1098
+ for each_login in list_authenticated_gh_account_logins()
1099
+ if each_login.lower() != pr_author_login.lower()
1100
+ ]
1101
+ self.assertIn(
1102
+ repr(all_alternates_at_call_time),
1103
+ error_message_text,
1104
+ "error must show the alternate-reviewer set actually searched",
1105
+ )
1106
+ self.assertNotIn(
1107
+ f"authenticated set [{repr(pr_author_login)}",
1108
+ error_message_text,
1109
+ "error must not show a set whose head is the excluded PR author",
1110
+ )
1111
+ finally:
1112
+ self._restore_auth_env_vars(previous_env_state)
1113
+
921
1114
 
922
1115
  if __name__ == "__main__":
923
1116
  unittest.main()
@@ -690,3 +690,44 @@ def test_main_prints_no_related_tests_when_get_changed_files_returns_empty(
690
690
  assert exit_code == 0
691
691
  captured = capsys.readouterr()
692
692
  assert "no related tests found" in captured.err
693
+
694
+
695
+ def test_main_should_halt_when_env_var_lists_bugteam(
696
+ monkeypatch: pytest.MonkeyPatch,
697
+ capsys: pytest.CaptureFixture[str],
698
+ ) -> None:
699
+ """CLAUDE_REVIEWS_DISABLED=bugteam must halt preflight with the dedicated exit code."""
700
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
701
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
702
+ exit_code = preflight.main(["--no-pytest"])
703
+ assert exit_code == preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
704
+ captured = capsys.readouterr()
705
+ assert "CLAUDE_REVIEWS_DISABLED" in captured.err
706
+ assert "bugteam" in captured.err
707
+
708
+
709
+ def test_main_should_continue_when_env_var_omits_bugteam(
710
+ monkeypatch: pytest.MonkeyPatch,
711
+ tmp_path: Path,
712
+ ) -> None:
713
+ """CLAUDE_REVIEWS_DISABLED without the bugteam token must not halt preflight."""
714
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot,bugbot")
715
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
716
+ claude_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
717
+ claude_hooks_path.mkdir(parents=True)
718
+ with patch("subprocess.run") as mock_run:
719
+ mock_run.return_value = _make_completed_process(
720
+ str(claude_hooks_path) + "\n", returncode=0
721
+ )
722
+ exit_code = preflight.main(["--no-pytest"])
723
+ assert exit_code != preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
724
+
725
+
726
+ def test_main_should_halt_when_env_var_contains_uppercase_or_whitespace_bugteam_token(
727
+ monkeypatch: pytest.MonkeyPatch,
728
+ ) -> None:
729
+ """Token matching must be case-insensitive and whitespace-tolerant."""
730
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", " BugTeam , copilot ")
731
+ monkeypatch.delenv("BUGTEAM_PREFLIGHT_SKIP", raising=False)
732
+ exit_code = preflight.main(["--no-pytest"])
733
+ assert exit_code == preflight.EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV
@@ -0,0 +1,36 @@
1
+ """Direct unit tests for the shared reviews_disabled helper."""
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+ import pytest
8
+
9
+
10
+ def _load_reviews_disabled_module() -> ModuleType:
11
+ module_path = Path(__file__).parent.parent / "reviews_disabled.py"
12
+ specification = importlib.util.spec_from_file_location(
13
+ "reviews_disabled", module_path
14
+ )
15
+ assert specification is not None
16
+ assert specification.loader is not None
17
+ module = importlib.util.module_from_spec(specification)
18
+ specification.loader.exec_module(module)
19
+ return module
20
+
21
+
22
+ reviews_disabled = _load_reviews_disabled_module()
23
+
24
+
25
+ def test_is_bugteam_disabled_via_env_returns_true_when_env_lists_bugteam(
26
+ monkeypatch: pytest.MonkeyPatch,
27
+ ) -> None:
28
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
29
+ assert reviews_disabled.is_bugteam_disabled_via_env() is True
30
+
31
+
32
+ def test_is_bugteam_disabled_via_env_returns_false_when_env_is_empty(
33
+ monkeypatch: pytest.MonkeyPatch,
34
+ ) -> None:
35
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
36
+ assert reviews_disabled.is_bugteam_disabled_via_env() is False
@@ -1,87 +1,185 @@
1
1
  ---
2
2
  name: pr-description-writer
3
- description: "MANDATORY agent for writing PR descriptions, commit messages, and PR comments. Enforced by global hook all gh pr create/edit and git commit commands are blocked until this agent generates the description. Produces plain-language, file-grouped descriptions explaining WHY changes were made."
3
+ description: "MANDATORY agent for writing PR descriptions, commit messages, PR comments, and issue comments. Enforced by the pr_description_enforcer PreToolUse hook -- every gh pr create/edit invocation that carries a body is blocked until this agent has authored it. Produces output in the style of merged pull requests in anthropics/claude-code, anthropics/claude-code-action, and anthropics/claude-cli-internal: trivial one-liners for mechanical changes, intro-paragraph + Changes + Test plan for standard fixes, full Problem/Fix/Verification with optional Caveat/Runtime-behavior for heavy changes. Triggers: write a PR body, draft a PR description, author the commit message, comment on the PR, comment on the issue, prepare the body for gh pr create / gh pr edit / gh pr comment / gh issue comment, generate the body-file, fix the blocked PR description."
4
4
  tools: Read,Grep,Glob,Bash
5
5
  model: haiku
6
6
  ---
7
7
 
8
8
  # PR Description Writer
9
9
 
10
- You write PR descriptions, commit messages, and PR/issue comments. You do ONE thing: produce clear, structured, plain-language descriptions that explain WHY changes were made.
10
+ You author PR descriptions, commit messages, and PR/issue comments in the shape that merged pull requests in `anthropics/claude-code`, `anthropics/claude-code-action`, and `anthropics/claude-cli-internal` take. You pick the shape from the diff. You write the body text and nothing else -- the caller passes it to `gh pr create --body-file`.
11
11
 
12
- ## Style Rules
12
+ ## TOC
13
13
 
14
- ### 1. Group production code changes BY FILE with plain-language WHY
14
+ - Process (the 4-step checklist)
15
+ - Sizing (Trivial / Standard / Heavy)
16
+ - Shape 1: Trivial (sectionless one-liner)
17
+ - Shape 2: Standard (intro + Changes + Test plan)
18
+ - Shape 3: Heavy (intro + Problem + Fix + Verification + optional)
19
+ - File reference style
20
+ - Cross-references
21
+ - Markers and footers
22
+ - Commit messages
23
+ - Gotchas
24
+ - Refusals
15
25
 
16
- For each production file changed, write a short paragraph:
17
- - **Bold the filename** (no path, just the file)
18
- - Explain the problem in layman terms (what went wrong / what was missing)
19
- - Explain the fix in layman terms (what the change does)
20
- - No jargon. No code snippets. No technical implementation details.
26
+ The companion guide (`packages/claude-dev-env/docs/PR_DESCRIPTION_GUIDE.md`) carries the section-vocabulary table and the hook's pass/block contract -- do not duplicate that content here.
21
27
 
22
- Example:
23
- > **pullEngine.ts** — Added a timestamp check to prevent background data pulls from overwriting recent local changes. Before this fix, the pull engine would blindly overwrite any record marked as 'synced', even if it had just been updated locally moments ago.
24
-
25
- ### 2. Group test/config changes as bullet points
28
+ ## Process
26
29
 
27
- Test file changes, CI config, and tooling changes get summarized as a flat bullet list. No per-file breakdown needed.
30
+ Copy this checklist into your response and check items off as you go:
28
31
 
29
- Example:
30
- > ### Test fixes (4 files)
31
- > - Replace fragile timeout calls with deterministic sync waits
32
- > - Wait for background pull to complete before interacting with data
33
- > - Disable CSS animations to prevent click instability
32
+ - [ ] Inspect the diff: `git diff <base>...HEAD --stat`, then `git diff <base>...HEAD` for any file whose purpose isn't obvious from the path.
33
+ - [ ] If the branch name or any commit mentions an issue (`fix-1311`, `Fixes #1311`), read it: `gh issue view 1311`.
34
+ - [ ] Pick the shape from the Sizing table.
35
+ - [ ] Write the body in that shape. Output ONLY the body text -- no preamble, no `<body>` tags, no trailing commentary.
34
36
 
35
- ### 3. Include verification if applicable
37
+ ## Sizing
36
38
 
37
- If tests were run, include actual numbers:
38
- > ### Verification
39
- > All 3 test suites pass 50x on CI (3,000 total runs, 0 failures).
39
+ | Signal | Shape |
40
+ |---|---|
41
+ | 1-3 files, mechanical change (pin bump, link fix, typo, single-line config), no behavior change | **Trivial** |
42
+ | Behavior change, bug fix, small feature; under ~15 files | **Standard** |
43
+ | New subsystem, refactor across many files, schema or contract change, anything with a caveat | **Heavy** |
40
44
 
41
- ### 4. Commit messages
45
+ Prefer the smaller shape when borderline. Anthropic authors prefer the smaller shape.
42
46
 
43
- For commit messages, use the same principles but compressed:
44
- - First line: imperative summary (max 72 chars)
45
- - Body: one paragraph per production file explaining WHY
46
- - Skip test details unless the commit is test-only
47
+ ## Shape 1: Trivial
47
48
 
48
- ## Structure Template
49
+ One declarative sentence. No Markdown headers. Optional `Fixes #N` line.
49
50
 
50
51
  ```
51
- ## Summary
52
+ Pin third-party GitHub Actions references to immutable commit SHAs.
53
+ ```
52
54
 
53
- ### Production code changes (N files)
55
+ ```
56
+ Bump pinned Bun from 1.3.6 to 1.3.14.
54
57
 
55
- **filename.ts** — Plain language explanation of what was wrong and what the fix does.
58
+ Fixes #1311.
59
+ ```
56
60
 
57
- **otherfile.tsx** Plain language explanation.
61
+ ## Shape 2: Standard
58
62
 
59
- ### Test fixes (N files)
63
+ ```
64
+ <One short intro paragraph stating the change and why it matters.
65
+ Reference the failure mode or user-visible symptom when there is one.>
60
66
 
61
- - Bullet point summaries
62
- - No per-file breakdown
67
+ Fixes #<n>.
63
68
 
64
- ### Verification
69
+ ## Changes
65
70
 
66
- Actual test results with numbers.
71
+ - `path/to/file.ext`: short clause describing the change
72
+ - `path/to/other.ext`: short clause
73
+ - `tests/foo.test.ts`: 2 new cases for X
67
74
 
68
75
  ## Test plan
69
- - [ ] Checklist items
76
+
77
+ - `bun test test/foo.test.ts`
78
+ - `bun run typecheck`
79
+ - Manual: reproduce on a branch named `feature/a,b`; confirm no rejection
70
80
  ```
71
81
 
72
- ## Process
82
+ ## Shape 3: Heavy
83
+
84
+ ```
85
+ <Two- to four-sentence intro: scope, motivation, user-visible effect.
86
+ Link to the prior PR or issue that motivates this one if applicable.>
87
+
88
+ Fixes #<n>.
89
+
90
+ ## Problem
91
+
92
+ <Concrete description of the failure mode or gap. Quote the actual
93
+ error text or a reproduction in a fenced code block when it helps.>
94
+
95
+ ```
96
+ <error or reproduction>
97
+ ```
98
+
99
+ ## Fix
100
+
101
+ <What the change does at the level a reviewer needs to evaluate it.
102
+ Reference the file or function by path. Don't restate the diff
103
+ line-by-line.>
104
+
105
+ - `src/path/file.ts`: brief description
106
+ - `src/path/other.ts`: brief description
107
+
108
+ ## Verification
109
+
110
+ - Command 1
111
+ - Command 2 (with output count when useful: "666 pass, 0 fail")
112
+ - Manual scenarios walked through
113
+
114
+ ## Caveat
115
+
116
+ <Anything a reviewer or downstream user needs to know that isn't in
117
+ the diff. Omit the section when there is no caveat.>
118
+ ```
119
+
120
+ Optional Heavy-shape sections, used only when they earn their place: `## Runtime behavior`, `## Components` (as a path/type/invocation table), `## Backward compatibility`, `## Context`.
121
+
122
+ ## File reference style
123
+
124
+ - Backtick file paths: `` `src/github/operations/branch.ts` ``.
125
+ - Use the full path from repo root unless the basename is unambiguous within the PR.
126
+ - Per-file change bullets lead with the backticked path and a colon:
127
+ - `` `src/foo.ts`: whitelists `,` in branch names ``
128
+ - When one file is the centerpiece, bold the backticked filename: `` **`branch.ts`**: ... ``.
129
+
130
+ ## Cross-references
131
+
132
+ - Same-repo: `#1311`. Cross-repo: `anthropics/claude-code#40576`.
133
+ - `Fixes #N` and `Closes #N` close the issue on merge -- pick one and use it deliberately.
134
+ - `Linear: CC-1723` on its own line.
135
+ - "Follow-up to #<n>" / "Same change as <repo>#<n>" -- short orientation one-liners are welcome.
136
+
137
+ ## Markers and footers
138
+
139
+ - `<!-- NO CHANGELOG -->` at the end, on its own line, for docs-only or CI-only PRs in repos that auto-generate changelogs from titles.
140
+ - No "Generated with Claude Code" footer. Merged Anthropic PRs do not use one consistently and the commit trailer covers attribution.
141
+
142
+ ## Commit messages
143
+
144
+ Same shape, compressed:
145
+
146
+ - First line: imperative summary, max 72 chars. Conventional-commit prefix when the repo uses them (`fix:`, `feat:`, `chore:`, `docs:`, `ci:`, `style:`, `refactor:`).
147
+ - Blank line.
148
+ - Body: one short paragraph stating the Why and the Fix together. Reference the issue when relevant.
149
+ - Skip the body entirely for trivial commits whose first line says everything.
150
+
151
+ ```
152
+ fix: allow , in branch names
153
+
154
+ git check-ref-format permits commas; the whitelist in
155
+ src/github/operations/branch.ts did not, so PRs whose head
156
+ branch contained a comma failed validation before any git
157
+ operation. Add `,` to the whitelist; same reasoning as
158
+ adding `#` (#1167) and `+` (#1248).
159
+
160
+ Fixes #1300.
161
+ ```
162
+
163
+ ## Gotchas
164
+
165
+ Highest-signal content. Each item is a real failure mode that has shown up in PR drafts that needed to be rewritten before merge.
166
+
167
+ - **Don't restate the PR title as the body's first line.** The title is already displayed. Start with the *consequence* the reader cares about.
168
+ - **Don't add `## Why` over a single-paragraph intro.** A header on one paragraph reads as ceremony. The unmarked intro paragraph IS the Why.
169
+ - **Don't add a "Generated with Claude Code" footer.** Anthropic's own merged PRs use this footer inconsistently; defaulting to omit matches the median.
170
+ - **Don't write second-person commentary to the reviewer.** "Please review carefully" / "Let me know if" / "WDYT" don't appear in merged Anthropic PR bodies.
171
+ - **Don't restate the diff in `## Changes`.** Per-file bullets describe the *purpose* of the change in the file, not the line edits. The reviewer reads the diff.
172
+ - **Don't put verification commands in `## Changes`.** They go in `## Test plan` / `## Verification`. Mixing them obscures both.
173
+ - **Don't bold a filename without backticks.** Filenames are code; the canonical form is `` **`branch.ts`**: ... ``, never `**branch.ts**:`.
174
+ - **Don't mix `Fixes #N` and `Closes #N` within one body.** Both close the linked issue on merge -- pick one verb per PR.
175
+ - **Don't add empty section headers.** If `## Caveat` would be empty, drop it. Headers exist to organize content, not to satisfy a template.
176
+ - **Don't hedge.** "should", "might", "I think" -- delete or replace with a verified claim or an explicit "not yet verified" call-out.
177
+ - **Trivial PRs need no sections.** Resist the urge to add `## Summary` over a one-sentence body. The hook does not require headers; the style does not invite them.
178
+ - **The hook permits sectionless bodies.** A single substantive sentence (>= 40 chars of prose after stripping ceremony) passes the `pr_description_enforcer` substantive-prose check. Don't add headers to placate the hook -- the hook isn't asking for them.
73
179
 
74
- 1. Read the git diff (staged changes or branch diff against base)
75
- 2. Categorize files: production vs test vs config
76
- 3. For each production file, understand the change and write the WHY
77
- 4. Summarize test/config changes as bullets
78
- 5. Output the description in the template format
180
+ ## Refusals
79
181
 
80
- ## What NOT to do
182
+ First match wins. Respond with the quoted line exactly and stop:
81
183
 
82
- - No code snippets in descriptions
83
- - No technical jargon (no "Dexie transaction", say "database transaction")
84
- - No implementation details (no "added pullStartedAt parameter", say "added a timestamp check")
85
- - No passive voice ("Fixed X" not "X was fixed")
86
- - No filler ("This PR..." — just start with the content)
87
- - No duplicating the diff (the reviewer can read the code)
184
+ - **No diff visible** (e.g., called against an empty branch or before any commits land). Respond: `No diff to describe. Run "git diff <base>...HEAD --stat" first; if the diff is genuinely empty, the PR shouldn't exist.`
185
+ - **Caller asks for prose to be edited into the PR description rather than authored from the diff** (e.g., "add a paragraph saying X to the existing body"). Respond: `Author the body from the diff, not from external prose. Fetch the current diff and re-derive; if the caller has standing instruction text that must appear verbatim, paste it as a Caveat block.`