Skip to content

Commit 4037ddb

Browse files
committed
Fix: Strip internal metadata header when syncing prompts
Adds _strip_metadata_header to BasePromptHandler to remove internal metadata (Prompt, ID, Status, etc.) before syncing prompt content to tool-specific files. This ensures that the synced files only contain the actual prompt content and not the manager's internal tracking data. Also includes a new test suite verifying this behavior.
1 parent e095097 commit 4037ddb

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

code_assistant_manager/prompts/base.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,59 @@ def get_prompt_file_path(
8989
return project_dir / filename
9090
return None
9191

92+
def _strip_metadata_header(self, content: str) -> str:
93+
"""
94+
Strip internal metadata header if present.
95+
96+
The metadata header typically looks like:
97+
Prompt: ...
98+
Description: ...
99+
Status: ...
100+
ID: ...
101+
102+
Content:
103+
104+
Args:
105+
content: The prompt content
106+
107+
Returns:
108+
Content with metadata header removed
109+
"""
110+
lines = content.splitlines()
111+
112+
# Find the "Content:" line
113+
content_line_idx = -1
114+
for i, line in enumerate(lines[:30]): # Check first 30 lines
115+
if line.strip() == "Content:":
116+
content_line_idx = i
117+
break
118+
119+
if content_line_idx != -1:
120+
# Check if preceding lines look like metadata
121+
# At least one line should start with known metadata keys
122+
header_slice = lines[:content_line_idx]
123+
has_metadata = False
124+
for line in header_slice:
125+
if line.startswith(
126+
("Prompt:", "ID:", "Description:", "Status:", "Imported from")
127+
):
128+
has_metadata = True
129+
break
130+
131+
if has_metadata:
132+
# Return content starting after "Content:" line
133+
# Skip "Content:" line
134+
start_idx = content_line_idx + 1
135+
136+
# Skip subsequent empty lines
137+
while start_idx < len(lines) and not lines[start_idx].strip():
138+
start_idx += 1
139+
140+
if start_idx < len(lines):
141+
return "\n".join(lines[start_idx:])
142+
143+
return content
144+
92145
def _normalize_header(self, content: str) -> str:
93146
"""
94147
Normalize the first line header to match this tool's name.
@@ -154,6 +207,9 @@ def sync_prompt(
154207
f"Tool '{self.tool_name}' does not support level '{level}'"
155208
)
156209

210+
# Strip metadata header if present
211+
content = self._strip_metadata_header(content)
212+
157213
# Normalize header to match this tool's name
158214
content = self._normalize_header(content)
159215

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from code_assistant_manager.prompts.claude import ClaudePromptHandler
6+
from code_assistant_manager.prompts.gemini import GeminiPromptHandler
7+
8+
9+
class TestPromptHeaderStripping:
10+
11+
@pytest.fixture
12+
def temp_dir(self, tmp_path):
13+
return tmp_path
14+
15+
def test_strip_metadata_header(self, temp_dir):
16+
handler = GeminiPromptHandler(user_path_override=temp_dir / "GEMINI.md")
17+
18+
content_with_header = """Prompt: Imported from User Gemini (2025-11-27 00:04)
19+
Description: Imported from /home/jzhu/.gemini/GEMINI.md
20+
Status: not default
21+
ID: gemini-user-f50022f5
22+
23+
Content:
24+
25+
# Gemini Code Assistant Instructions
26+
27+
## Role
28+
You are an expert.
29+
"""
30+
31+
# Sync the prompt
32+
synced_path = handler.sync_prompt(content_with_header, level="user")
33+
34+
# Read the file
35+
synced_content = synced_path.read_text()
36+
37+
# Verify header is gone
38+
assert "Prompt: Imported from User" not in synced_content
39+
assert "Status: not default" not in synced_content
40+
assert "Content:" not in synced_content
41+
42+
# Verify content starts with the markdown header
43+
assert synced_content.startswith("# Gemini Code Assistant Instructions")
44+
assert "## Role" in synced_content
45+
46+
def test_normalize_header_after_stripping(self, temp_dir):
47+
# Use Claude handler to verify header renaming
48+
handler = ClaudePromptHandler(user_path_override=temp_dir / "CLAUDE.md")
49+
50+
content_with_header = """Prompt: Some Prompt
51+
ID: 123
52+
53+
Content:
54+
55+
# Gemini Code Assistant Instructions
56+
57+
Some content.
58+
"""
59+
60+
synced_path = handler.sync_prompt(content_with_header, level="user")
61+
synced_content = synced_path.read_text()
62+
63+
# Verify header is stripped
64+
assert "Prompt: Some Prompt" not in synced_content
65+
66+
# Verify header is renamed to Claude
67+
assert synced_content.startswith("# Claude Code Assistant Instructions")
68+
69+
def test_no_header_strip_if_no_content_delimiter(self, temp_dir):
70+
handler = GeminiPromptHandler(user_path_override=temp_dir / "GEMINI.md")
71+
72+
content_simple = """# Just a normal prompt
73+
74+
With some content.
75+
"""
76+
77+
synced_path = handler.sync_prompt(content_simple, level="user")
78+
synced_content = synced_path.read_text()
79+
80+
assert synced_content == content_simple

0 commit comments

Comments
 (0)