427905a00dc6799d3c8d750de5d9376bccc3c972
jnavarr5
  Thu Apr 9 15:49:33 2026 -0700
Add tracker name resolution to redmineCli list command, refs #37339

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git src/utils/redmineCli src/utils/redmineCli
index a23e921b16e..909628e2c0b 100755
--- src/utils/redmineCli
+++ src/utils/redmineCli
@@ -26,30 +26,54 @@
 import urllib.parse
 import urllib.request
 from datetime import datetime
 
 # ---------------------------------------------------------------------------
 # Constants
 # ---------------------------------------------------------------------------
 
 DEFAULT_REDMINE = "https://redmine.gi.ucsc.edu"
 DEFAULT_PROJECT = "maillists"
 
 TRACKER_MLQ = 7
 PRIORITY_UNPRIORITIZED = 12
 STATUS_NEW = 1
 
+# Name -> Redmine tracker ID mapping (case-insensitive lookup in resolve_tracker)
+TRACKER_IDS = {
+    "bug": 21,
+    "feature": 23,
+    "track": 11,
+    "hub": 45,
+    "data sets": 46, "datasets": 46,
+    "to do": 10, "todo": 10,
+    "docs": 25,
+    "assembly": 24,
+    "process": 12,
+    "meeting": 28,
+    "cr": 26,
+    "cgi build": 33,
+    "mlq": 7,
+    "mlq off list": 15,
+    "suggestion box": 44,
+    "github": 48,
+    "information": 35, "info": 35,
+    "build patch": 36,
+    "release": 47,
+    "housekeeping": 49,
+}
+
 # Name -> Redmine status ID mapping (case-insensitive lookup in resolve_status)
 STATUS_IDS = {
     "new": 1,
     "looking for dev": 37,
     "snoozed": 34,
     "limbo": 36,
     "researching/exploratory": 35, "researching": 35, "exploratory": 35,
     "masked 2bit file": 29,
     "initial sequence": 25,
     "minimal browser": 26,
     "docs in progress": 27,
     "on deck": 33,
     "in progress": 2,
     "stalled": 13,
     "qa ready": 10, "qa": 10,
@@ -244,30 +268,41 @@
              "Run 'redmineCli users' to see available names and IDs.")
 
 
 def resolve_status(name_or_id):
     """Resolve a status name to a Redmine status ID. Accepts name or numeric ID."""
     if name_or_id.isdigit():
         return int(name_or_id)
     key = name_or_id.lower().strip()
     if key in STATUS_IDS:
         return STATUS_IDS[key]
     sys.exit(f"Error: unknown status '{name_or_id}'. Known statuses: "
              + ", ".join(sorted(k for k in STATUS_IDS if " " in k or not any(
                  k2 != k and STATUS_IDS[k2] == STATUS_IDS[k] for k2 in STATUS_IDS))))
 
 
+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."""
     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:
@@ -441,31 +476,31 @@
         "limit": str(args.limit),
         "offset": str(args.offset),
         "sort": args.sort,
     }
 
     if args.status:
         params["status_id"] = args.status
 
     if args.assigned_to:
         if args.assigned_to.lower() == "me":
             params["assigned_to_id"] = "me"
         else:
             params["assigned_to_id"] = str(resolve_user(args.assigned_to))
 
     if args.tracker:
-        params["tracker_id"] = args.tracker
+        params["tracker_id"] = resolve_tracker(args.tracker)
 
     if args.search:
         params["subject"] = f"~{args.search}"
 
     query = urllib.parse.urlencode(params)
     data = api_get(args.base_url, f"/issues.json?{query}", args.api_key)
 
     issues = data.get("issues", [])
     total = data.get("total_count", 0)
 
     if not issues:
         print("No issues found.")
         return
 
     out = []