using System.Globalization; using ClaudeDo.Data.Repositories; namespace ClaudeDo.Worker.External; /// /// Resolves a task-id MCP parameter that may be a display number (#123 or bare 123) /// into the task's GUID. A GUID is never all-digits, so the two forms never collide. Anything /// else — including an empty string, which update_task's dependsOnTaskId uses as a /// "clear the link" sentinel — passes through untouched. /// internal static class TaskIdResolver { public static async Task ResolveAsync(TaskRepository tasks, string idOrNumber, CancellationToken ct = default) { return (await ResolveCoreAsync(tasks, idOrNumber, ct))!; } public static async Task ResolveOptionalAsync(TaskRepository tasks, string? idOrNumber, CancellationToken ct = default) { return idOrNumber is null ? null : await ResolveCoreAsync(tasks, idOrNumber, ct); } public static async Task> ResolveManyAsync( TaskRepository tasks, IReadOnlyList idsOrNumbers, CancellationToken ct = default) { var resolved = new string[idsOrNumbers.Count]; for (var i = 0; i < idsOrNumbers.Count; i++) resolved[i] = await ResolveAsync(tasks, idsOrNumbers[i], ct); return resolved; } private static async Task ResolveCoreAsync(TaskRepository tasks, string idOrNumber, CancellationToken ct) { if (idOrNumber.Length == 0) return idOrNumber; var candidate = idOrNumber[0] == '#' ? idOrNumber[1..] : idOrNumber; if (!int.TryParse(candidate, NumberStyles.None, CultureInfo.InvariantCulture, out var number)) return idOrNumber; var task = await tasks.GetByNumberAsync(number, ct) ?? throw new InvalidOperationException($"no task with number {number}"); return task.Id; } }