From 1c6767ef1369350806215ac4a133c41f9d0abc1d Mon Sep 17 00:00:00 2001 From: viktormarinho Date: Fri, 11 Sep 2026 18:05:05 -0300 Subject: [PATCH] fix(task-board): a stale card repo must not veto binding the sole repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pickSoleTaskRepo`'s `preferredRepo` (added in #7159) filtered the choices, and an empty filter result meant no bind. But `task.repo` is free text that goes stale — a renamed repository, a value from the github-connection era, a typo — so an org with exactly ONE repo stopped binding it whenever a card named something else, falling back to the mid-run `TASK_ADD_REPO` pick and costing a turn. That org bound its sole repo before the parameter existed. The preference is now a hint: it narrows when it matches, and is discarded when it doesn't. Multi-repo behavior is unchanged — a preference matching nothing or matching ambiguously still declines to bind and defers to the mid-run pick, which the existing tests cover. Unflagged on purpose, like the regression it repairs: this sits on the Super Agent dispatch path for every org, so leaving it half-applied behind a flag would keep the broken case live. --- .../tools/task-board/claude-code-task-run.test.ts | 10 ++++++++++ .../src/tools/task-board/claude-code-task-run.ts | 15 +++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/api/src/tools/task-board/claude-code-task-run.test.ts b/apps/api/src/tools/task-board/claude-code-task-run.test.ts index 317cd12c7e..7c1a25d458 100644 --- a/apps/api/src/tools/task-board/claude-code-task-run.test.ts +++ b/apps/api/src/tools/task-board/claude-code-task-run.test.ts @@ -281,6 +281,16 @@ describe("pickSoleTaskRepo", () => { ), ).toBeNull(); }); + /** The card's `repo` is free text, so it goes stale: a renamed repository, a + * value from the github-connection era, a typo. It must narrow the choice, + * never veto it — an org with exactly one repo bound that repo before this + * parameter existed, and still has to. */ + test("a preference that matches nothing falls back to the sole repo", () => { + const sole = [choice("one", "acme", "web")]; + expect(pickSoleTaskRepo(sole, "acme/renamed-away")?.id).toBe("one"); + expect(pickSoleTaskRepo(sole)?.id).toBe("one"); + }); + test("no clonable repo is not eligible", () => { expect(pickSoleTaskRepo([])).toBeNull(); }); diff --git a/apps/api/src/tools/task-board/claude-code-task-run.ts b/apps/api/src/tools/task-board/claude-code-task-run.ts index 48cc7ffa43..ef62b68f9b 100644 --- a/apps/api/src/tools/task-board/claude-code-task-run.ts +++ b/apps/api/src/tools/task-board/claude-code-task-run.ts @@ -75,16 +75,23 @@ export interface TaskRepo { export function pickSoleTaskRepo( choices: RepoChoice[], /** The card's own `repo`, when it has one: narrow to it first, so a - * multi-repo org still binds a checkout before dispatch. An unknown or - * ambiguous name narrows to nothing and falls back to the mid-run pick. */ + * multi-repo org still binds a checkout before dispatch. + * + * A HINT, never a veto. `repo` is free text that goes stale — a renamed + * repository, a value from the github-connection era, a typo — and letting + * a stale one empty the set would stop binding the sole repo of a + * single-repo org, which this bound before the parameter existed. So a + * preference that matches nothing is discarded, not honored. */ preferredRepo?: string, ): TaskRepo | null { - if (preferredRepo) - choices = choices.filter( + if (preferredRepo) { + const narrowed = choices.filter( (choice) => `${choice.owner}/${choice.name}`.toLowerCase() === preferredRepo.toLowerCase(), ); + if (narrowed.length > 0) choices = narrowed; + } if (choices.length !== 1) return null; const chosen = choices[0]!; return {