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()