Skip to content

Commit 06880d4

Browse files
committed
fix: strip SAAR markers from team_rules -- prevents inception loop and triple duplication
1 parent 54f806b commit 06880d4

3 files changed

Lines changed: 58 additions & 13 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!-- SAAR:AUTO-START -->
22
# CLAUDE.md -- saar
33

4-
252 functions, 38 classes.
4+
253 functions, 38 classes.
55
Async adoption: 21%.
66
Type hint coverage: 92%.
77

@@ -35,11 +35,11 @@ import os
3535
These files have the most dependents -- understand them before editing:
3636

3737
- `saar/models.py` (14 dependents)
38+
- `saar/formatters/agents_md.py` (4 dependents)
3839
- `saar/formatters/_tribal.py` (4 dependents)
39-
- `saar/interview.py` (3 dependents)
4040
- `saar/cli.py` (3 dependents)
41-
- `saar/formatters/agents_md.py` (3 dependents)
4241
- `saar/formatters/claude_md.py` (3 dependents)
42+
- `saar/interview.py` (3 dependents)
4343
- `saar/dependency_analyzer.py` (2 dependents)
4444
- `saar/style_analyzer.py` (2 dependents)
4545

saar/formatters/agents_md.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,46 @@
1212
from saar.models import CodebaseDNA
1313
from saar.formatters._tribal import render_tribal_knowledge
1414

15+
# Markers we strip from team_rules before embedding -- prevents nested markers
16+
# and the inception loop where saar reads its own output as input
17+
_SAAR_MARKER_START = "<!-- SAAR:AUTO-START -->"
18+
_SAAR_MARKER_END = "<!-- SAAR:AUTO-END -->"
19+
20+
# Max chars to include from hand-written team rules.
21+
# The auto-detected DNA above already covers conventions -- team rules should
22+
# add what's MISSING (data flow, key services, commit rules), not repeat them.
23+
_TEAM_RULES_MAX_CHARS = 3000
24+
25+
26+
def _clean_team_rules(raw: str) -> str:
27+
"""Strip SAAR markers and truncate team rules before embedding.
28+
29+
Why: team_rules comes from reading CLAUDE.md or .cursorrules verbatim.
30+
If CLAUDE.md was previously generated by saar, it contains SAAR:AUTO-START
31+
markers, causing nested markers and triple duplication on re-runs.
32+
33+
We strip the auto-generated block (between markers) and keep only the
34+
human-written sections that live outside the markers.
35+
"""
36+
# strip auto-generated blocks between markers -- those are already
37+
# covered by the DNA sections above, no need to repeat them
38+
result = raw
39+
while _SAAR_MARKER_START in result and _SAAR_MARKER_END in result:
40+
start = result.find(_SAAR_MARKER_START)
41+
end = result.find(_SAAR_MARKER_END, start)
42+
if start == -1 or end == -1:
43+
break
44+
result = result[:start] + result[end + len(_SAAR_MARKER_END):]
45+
46+
result = result.strip()
47+
48+
# truncate if still very long -- human rules should be concise
49+
if len(result) > _TEAM_RULES_MAX_CHARS:
50+
result = result[:_TEAM_RULES_MAX_CHARS].rstrip()
51+
result += "\n\n*(truncated -- see full file for remaining rules)*"
52+
53+
return result
54+
1555

1656
def render_agents_md(dna: CodebaseDNA) -> str:
1757
"""Render DNA as an AGENTS.md file."""
@@ -164,11 +204,13 @@ def render_agents_md(dna: CodebaseDNA) -> str:
164204
if tribal:
165205
lines.append(tribal)
166206

167-
# -- team rules verbatim --
207+
# -- team rules verbatim (human-written sections only) --
168208
if dna.team_rules:
169-
lines.append("\n## Project-Specific Rules\n")
170-
if dna.team_rules_source:
171-
lines.append(f"*From `{dna.team_rules_source}`*\n")
172-
lines.append(dna.team_rules)
209+
cleaned = _clean_team_rules(dna.team_rules)
210+
if cleaned:
211+
lines.append("\n## Project-Specific Rules\n")
212+
if dna.team_rules_source:
213+
lines.append(f"*From `{dna.team_rules_source}`*\n")
214+
lines.append(cleaned)
173215

174216
return "\n".join(lines) + "\n"

saar/formatters/claude_md.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
from saar.models import CodebaseDNA
88
from saar.formatters._tribal import render_tribal_knowledge
9+
from saar.formatters.agents_md import _clean_team_rules
910

1011

1112
def render_claude_md(dna: CodebaseDNA) -> str:
@@ -139,11 +140,13 @@ def render_claude_md(dna: CodebaseDNA) -> str:
139140
if tribal:
140141
lines.append(tribal)
141142

142-
# -- team rules (verbatim if they exist) --
143+
# -- team rules (human-written sections only) --
143144
if dna.team_rules:
144-
lines.append("\n## Team Rules\n")
145-
if dna.team_rules_source:
146-
lines.append(f"*Imported from `{dna.team_rules_source}`*\n")
147-
lines.append(dna.team_rules)
145+
cleaned = _clean_team_rules(dna.team_rules)
146+
if cleaned:
147+
lines.append("\n## Team Rules\n")
148+
if dna.team_rules_source:
149+
lines.append(f"*Imported from `{dna.team_rules_source}`*\n")
150+
lines.append(cleaned)
148151

149152
return "\n".join(lines) + "\n"

0 commit comments

Comments
 (0)