claude-dev-env 8.78.0 → 8.78.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.
- package/.agents/skills/pr-lifecycle/reference/review-loop-before-merge.md +1 -1
- package/hooks/blocking/merge_review_gate.py +4 -3
- package/hooks/blocking/test_merge_review_gate.py +19 -2
- package/hooks/features/conduct-gates.md +2 -2
- package/hooks/hooks_constants/merge_review_gate_constants.py +3 -2
- package/package.json +1 -1
|
@@ -8,4 +8,4 @@ A pull request the agent made merges only after a code review of its head comes
|
|
|
8
8
|
4. Repeat until a pass records `CLEAN`.
|
|
9
9
|
5. Merge pinned to that head: `expectedHeadSha` on the merge tool, or `--match-head-commit` on `gh pr merge`.
|
|
10
10
|
|
|
11
|
-
`hooks/blocking/merge_review_gate.py` denies any other merge
|
|
11
|
+
`hooks/blocking/merge_review_gate.py` denies any other merge. In a merge-queue repository, enqueue with `gh pr merge <number> --repo <owner>/<name> --auto --match-head-commit <head sha>`; auto-merge that names no head is denied. A repository whose own CI and merge queue review and merge its pull requests is listed in `CLAUDE_MERGE_REVIEW_EXEMPT_REPOSITORIES`, comma separated as `owner/name`, and skips the gate.
|
|
@@ -8,7 +8,8 @@ names that exact head::
|
|
|
8
8
|
merge, receipt at abc123, merge pinned to def456 -> deny, review again
|
|
9
9
|
merge, receipt at abc123, merge names no head -> deny, pin it
|
|
10
10
|
merge, no receipt or the receipt counts findings -> deny, run the loop
|
|
11
|
-
auto-merge
|
|
11
|
+
auto-merge or enqueue pinned to the clean head -> allow
|
|
12
|
+
auto-merge that names no head -> deny, pin it
|
|
12
13
|
repository listed in CLAUDE_MERGE_REVIEW_EXEMPT_REPOSITORIES -> allow
|
|
13
14
|
|
|
14
15
|
The deny reason names the review command CLAUDE_MERGE_REVIEW_COMMAND sets,
|
|
@@ -256,11 +257,11 @@ def deny_reason(request: MergeRequest) -> str | None:
|
|
|
256
257
|
if _is_exempt(request):
|
|
257
258
|
return None
|
|
258
259
|
fields = {"slug": request.slug, "number": request.number}
|
|
259
|
-
if request.is_auto_merge:
|
|
260
|
-
return AUTO_MERGE_REASON_TEMPLATE.format(**fields)
|
|
261
260
|
clean_head = _clean_head(request)
|
|
262
261
|
if clean_head is None:
|
|
263
262
|
return NO_CLEAN_REVIEW_REASON_TEMPLATE.format(review_command=_review_command(), **fields)
|
|
263
|
+
if request.pinned_head is None and request.is_auto_merge:
|
|
264
|
+
return AUTO_MERGE_REASON_TEMPLATE.format(head=clean_head, **fields)
|
|
264
265
|
if request.pinned_head is None:
|
|
265
266
|
return UNPINNED_MERGE_REASON_TEMPLATE.format(head=clean_head, **fields)
|
|
266
267
|
if not _same_commit(clean_head, request.pinned_head):
|
|
@@ -68,13 +68,13 @@ def test_should_deny_an_unpinned_merge_after_a_clean_review() -> None:
|
|
|
68
68
|
assert "names no head" in _reason(_merge_tool())
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def test_should_deny_auto_merge_that_names_no_head() -> None:
|
|
72
72
|
merge_review_receipt.record_review("acme/tools", 12, CLEAN_HEAD, 0)
|
|
73
73
|
payload = {
|
|
74
74
|
"tool_name": "mcp__github__enable_pr_auto_merge",
|
|
75
75
|
"tool_input": {"owner": "acme", "repo": "tools", "pullNumber": 12},
|
|
76
76
|
}
|
|
77
|
-
assert "
|
|
77
|
+
assert f"--match-head-commit {CLEAN_HEAD}" in _reason(payload)
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def test_should_allow_any_merge_in_an_exempt_repository(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -113,3 +113,20 @@ def test_should_ignore_commands_that_merge_nothing() -> None:
|
|
|
113
113
|
def test_should_name_the_configured_review_command(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
114
114
|
monkeypatch.setenv("CLAUDE_MERGE_REVIEW_COMMAND", "/team-review")
|
|
115
115
|
assert "`/team-review`" in _reason(_merge_tool(CLEAN_HEAD))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_should_allow_auto_merge_pinned_to_the_clean_head() -> None:
|
|
119
|
+
merge_review_receipt.record_review("acme/tools", 12, CLEAN_HEAD, 0)
|
|
120
|
+
command = f"gh pr merge 12 --repo acme/tools --auto --match-head-commit {CLEAN_HEAD}"
|
|
121
|
+
assert _reason(_bash(command)) is None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_should_deny_pinned_auto_merge_with_no_clean_review() -> None:
|
|
125
|
+
command = f"gh pr merge 12 --repo acme/tools --auto --match-head-commit {CLEAN_HEAD}"
|
|
126
|
+
assert "no clean review" in _reason(_bash(command))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_should_deny_auto_merge_pinned_to_a_newer_head() -> None:
|
|
130
|
+
merge_review_receipt.record_review("acme/tools", 12, CLEAN_HEAD, 0)
|
|
131
|
+
command = f"gh pr merge 12 --repo acme/tools --auto --match-head-commit {NEW_HEAD}"
|
|
132
|
+
assert "Review the new head" in _reason(_bash(command))
|
|
@@ -11,7 +11,7 @@ This family stops a chat reply, edited message, or tool call when its payload br
|
|
|
11
11
|
- `blocking/step_note_gate.py` requires a status line before a tool call while its opt-in flag is on.
|
|
12
12
|
- `blocking/verify_before_acting.py` blocks a completed mutating call when the agent's reasoning contains an unchecked claim.
|
|
13
13
|
- `blocking/pr_lifecycle_skill_gate.py` denies a commit, push, pull request action, or merge until the `pr-lifecycle` skill appears in the transcript since the last compaction.
|
|
14
|
-
- `blocking/merge_review_gate.py` denies an agent's merge until a review pass on the merged head reported zero findings. It reads the receipt `scripts/merge_review_receipt.py` writes, requires the merge to be pinned to that head,
|
|
14
|
+
- `blocking/merge_review_gate.py` denies an agent's merge until a review pass on the merged head reported zero findings. It reads the receipt `scripts/merge_review_receipt.py` writes, requires the merge, auto-merge or merge-queue entry to be pinned to that head, and skips repositories listed in `CLAUDE_MERGE_REVIEW_EXEMPT_REPOSITORIES`. Its deny reason names the review command `CLAUDE_MERGE_REVIEW_COMMAND` sets.
|
|
15
15
|
- `blocking/artifact_dark_mode_gate.py` denies an Artifact page publish when the page, inside the publish wrapper, keeps a light body or shows text under 4.5:1 contrast in dark mode. `blocking/artifact_dark_mode_render.cjs` renders the page in headless Chromium for it.
|
|
16
16
|
|
|
17
17
|
## When it fires
|
|
@@ -39,7 +39,7 @@ Preconditions:
|
|
|
39
39
|
- **Step note.** Input is a tool call after a transcript message without a status line while the flag is on. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_step_note_gate.py -q`. The adjacent test observes a block until a status line appears.
|
|
40
40
|
- **Checked claim.** Input is a Write after hedged reasoning in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_verify_before_acting.py -q`. The adjacent test observes a block and a logged blocked outcome.
|
|
41
41
|
- **Lifecycle skill.** Input is a `git commit` or `gh pr create` command with no `pr-lifecycle` invocation in the transcript. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_pr_lifecycle_skill_gate.py -q`. The adjacent test observes a denial, then a silent allow once the skill or its slash command appears.
|
|
42
|
-
- **Clean review.** Input is a merge with no receipt, a receipt with findings, a merge pinned to a newer head, an unpinned merge, or auto-merge. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_merge_review_gate.py -q`. The adjacent test observes a denial for each; a merge pinned to the clean head and a merge in an exempt repository pass.
|
|
42
|
+
- **Clean review.** Input is a merge with no receipt, a receipt with findings, a merge pinned to a newer head, an unpinned merge, or auto-merge with no head. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_merge_review_gate.py -q`. The adjacent test observes a denial for each; a merge or auto-merge pinned to the clean head and a merge in an exempt repository pass.
|
|
43
43
|
- **Dark mode page.** Input is an Artifact publish of a page that themes `html` but leaves `body` to the publish wrapper. Run `python -m pytest packages/claude-dev-env/hooks/blocking/test_artifact_dark_mode_gate.py -q`. The adjacent test observes a denial that names the text and its contrast; a page that themes `body` passes. The render tests skip on a host with no Node Playwright and Chromium.
|
|
44
44
|
|
|
45
45
|
## Gotchas
|
|
@@ -63,8 +63,9 @@ STALE_REVIEW_REASON_TEMPLATE = (
|
|
|
63
63
|
"Review the new head until a pass reports zero findings, record it, then merge."
|
|
64
64
|
)
|
|
65
65
|
AUTO_MERGE_REASON_TEMPLATE = (
|
|
66
|
-
"Auto-merge on {slug}#{number}
|
|
67
|
-
"
|
|
66
|
+
"Auto-merge on {slug}#{number} names no head, so it could merge a head no review "
|
|
67
|
+
"has seen. Run `gh pr merge {number} --repo {slug} --auto --match-head-commit {head}`, "
|
|
68
|
+
"which arms auto-merge or joins the merge queue only while the head is {head}."
|
|
68
69
|
)
|
|
69
70
|
UNKNOWN_TARGET_REASON = (
|
|
70
71
|
"This merge names no repository and pull request number the review gate can read. "
|