Skip to content

Commit 34b5f33

Browse files
fix: handle negated pull request conflict guidance (#119)
Co-authored-by: CoderDeltaLAN <CoderDeltaLAN@users.noreply.github.com>
1 parent 6170786 commit 34b5f33

3 files changed

Lines changed: 60 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ This project has a published GitHub Release line, but no stable support or API g
8787

8888
### Fixed
8989

90+
- Prevented negated pull request guidance from being treated as a pull request requirement in conflict detection.
9091
- Made the post-release audit CLI version smoke derive the expected version from `pyproject.toml` instead of hardcoding `0.3.0`.
9192
- Scoped governance finding suppression to same-line negation or approval cues so adjacent safe guidance no longer hides unrelated risky instructions.
9293
- Reject symlinked supported instruction files and harden `init --write` temporary and backup paths against symlink escapes.

src/agent_rules_kit/conflicts.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class _ConflictRule:
6363
re.compile(r"\b(commit|push)\s+directly\s+to\s+main\b", re.IGNORECASE),
6464
re.compile(r"\bdirect\s+push(?:es)?\s+to\s+main\s+(are\s+)?(allowed|ok|fine)\b", re.IGNORECASE), # noqa: E501
6565
re.compile(r"\bmerge\s+without\s+(review|approval)\b", re.IGNORECASE),
66+
re.compile(r"\b(do not|don't|never|avoid)\b.{0,80}\buse\s+pull\s+requests?\b", re.IGNORECASE), # noqa: E501
67+
re.compile(r"\b(no\s+PR|PR\s+is\s+not|required\s+PR\s+is\s+not|pull\s+requests?\s+are\s+not)\b.{0,80}\b(required|needed|mandatory)\b", re.IGNORECASE), # noqa: E501
6668
),
6769
),
6870
_ConflictRule(
@@ -213,10 +215,29 @@ def build_conflict_report(
213215

214216

215217
def _matches_conflict_rule(stripped: str, rule: _ConflictRule) -> bool:
216-
if rule.polarity == "allow" and _has_negated_guidance(stripped):
218+
matched = any(pattern.search(stripped) for pattern in rule.patterns)
219+
if not matched:
217220
return False
218221

219-
return any(pattern.search(stripped) for pattern in rule.patterns)
222+
if rule.topic == "main integration" and _has_negated_pr_boundary(stripped):
223+
return rule.polarity == "allow"
224+
225+
return not (rule.polarity == "allow" and _has_negated_guidance(stripped))
226+
227+
228+
def _has_negated_pr_boundary(stripped: str) -> bool:
229+
return bool(
230+
re.search(
231+
r"\b(do not|don't|never|avoid)\b.{0,80}\buse\s+pull\s+requests?\b",
232+
stripped,
233+
re.IGNORECASE,
234+
)
235+
or re.search(
236+
r"\b(no\s+PR|PR\s+is\s+not|required\s+PR\s+is\s+not|pull\s+requests?\s+are\s+not)\b.{0,80}\b(required|needed|mandatory)\b",
237+
stripped,
238+
re.IGNORECASE,
239+
)
240+
)
220241

221242

222243
def _has_negated_guidance(stripped: str) -> bool:

tests/test_conflicts.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,42 @@ def test_ignores_aligned_pr_guidance(self) -> None:
4545

4646
self.assertEqual(report.conflict_group_count, 0)
4747

48+
49+
def test_does_not_treat_negated_pr_guidance_as_pr_requirement(self) -> None:
50+
with tempfile.TemporaryDirectory() as tmp_dir:
51+
root = Path(tmp_dir)
52+
(root / "AGENTS.md").write_text(
53+
"# Agent instructions\n\n- Commit directly to main.\n",
54+
encoding="utf-8",
55+
)
56+
(root / "CLAUDE.md").write_text(
57+
"# Claude instructions\n\n- Do not use pull requests for changes to main.\n",
58+
encoding="utf-8",
59+
)
60+
61+
report = build_conflict_report(root, discover_instruction_files(root))
62+
63+
self.assertEqual(report.conflict_group_count, 0)
64+
65+
def test_reports_negated_pr_guidance_against_pr_requirement(self) -> None:
66+
with tempfile.TemporaryDirectory() as tmp_dir:
67+
root = Path(tmp_dir)
68+
(root / "AGENTS.md").write_text(
69+
"# Agent instructions\n\n- Do not use pull requests for changes to main.\n",
70+
encoding="utf-8",
71+
)
72+
(root / "CLAUDE.md").write_text(
73+
"# Claude instructions\n\n- Use pull requests for changes to main.\n",
74+
encoding="utf-8",
75+
)
76+
77+
report = build_conflict_report(root, discover_instruction_files(root))
78+
79+
self.assertEqual(report.conflict_group_count, 1)
80+
self.assertEqual(report.groups[0].topic, "main integration")
81+
self.assertEqual([location.path for location in report.groups[0].allow_locations], ["AGENTS.md"]) # noqa: E501
82+
self.assertEqual([location.path for location in report.groups[0].block_locations], ["CLAUDE.md"]) # noqa: E501
83+
4884
def test_rejects_symlinked_instruction_files(self) -> None:
4985
with tempfile.TemporaryDirectory() as tmp_dir:
5086
root = Path(tmp_dir)

0 commit comments

Comments
 (0)