993da626132958795cab63a9b26d64ce2052f40d
lrnassar
  Tue Apr 21 16:51:13 2026 -0700
Make redmineCli prepend_attribution idempotent. refs #37339

Skip adding the '**From Claude:**' header if the body already begins
with a From Claude attribution line (any bold/italic asterisk variant,
case-insensitive). Fixes the periodic doubled header when Claude models
mimic prior journal entries that already carried the prefix.

diff --git src/utils/redmineCli src/utils/redmineCli
index 5b6b24c76c2..961ac747fa9 100755
--- src/utils/redmineCli
+++ src/utils/redmineCli
@@ -319,31 +319,38 @@
              f"Available for project {project_id}: {all_names}")
 
 
 def resolve_tracker(name_or_id):
     """Resolve a tracker name to a Redmine tracker ID. Accepts name or numeric ID."""
     if str(name_or_id).isdigit():
         return int(name_or_id)
     key = str(name_or_id).lower().strip()
     if key in TRACKER_IDS:
         return TRACKER_IDS[key]
     sys.exit(f"Error: unknown tracker '{name_or_id}'. Known trackers: "
              + ", ".join(sorted(set(TRACKER_IDS.keys()))))
 
 
 def prepend_attribution(text):
-    """Prepend 'From Claude:' attribution to text for write operations."""
+    """Prepend 'From Claude:' attribution to text for write operations.
+
+    Idempotent: if the text already begins with a 'From Claude:' attribution
+    line (e.g. '**From Claude:**', '***From Claude:***'), return it unchanged
+    so the header is not duplicated when Claude models include it in the body.
+    """
+    if text and re.match(r'^\s*\*+\s*From Claude:?\s*\*+', text, re.IGNORECASE):
+        return text
     return ATTRIBUTION + text
 
 
 def strip_emoji(text):
     """Strip 4-byte Unicode (emoji) that Redmine's MySQL may reject."""
     if not text:
         return text
     return re.sub(r'[\U00010000-\U0010FFFF]', '', text)
 
 
 def read_text_input(direct, from_file):
     """Read text from --message/--description or --message-file/--description-file."""
     if from_file:
         if from_file == "-":
             return sys.stdin.read()