Skip to content

feat: name a worktree by the GitHub issue it is for - #459

Merged
omartelo merged 2 commits into
mainfrom
feat/worktree-from-issue
Sep 5, 2026
Merged

omartelo merged 2 commits into
mainfrom
feat/worktree-from-issue

Conversation

@omartelo

@omartelo omartelo commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

lich lists a repository's pull requests, shows Files changed, comments, resolves conflicts and merges — the right-hand side of the flow. The left-hand side did not exist: an issue was not work. You opened a worktree, named it by hand, and typed the context in at the agent's prompt.

The New worktree dialog's name field now takes an issue beside the branch names and plain words it already took.

What it does

Type #128, or paste the URL copied out of the browser. lich looks the issue up through gh on the project's own account (vcs.account), prints its title and the exact branch it will create under the field before anything exists, and hands the issue to the session that opens on it — number, title, link and body, waiting unsent at the agent's prompt.

Measured against this repository, every state of that one field:

typed shown under the field
#381 #381 Sandbox backend for Windows (unelevated write protection) / Branch: 381-sandbox-backend-for-windows
#453 (a pull request) #453 is a pull request, not an issue — open it from the Pulls screen. / Branch: 453
#999999 GitHub has no issue with that number. / Branch: 999999
fix the auth redirect Branch: fix-the-auth-redirect
381 Branch: 381
(empty) Branch: <auto-generated>

Two refusals, both because the field is a branch name first

  • A bare number stays a branch name. # is the marker, so there is one field with one meaning rather than a mode to be in.
  • A pull request's number is refused, naming the screen that does open pull requests. GitHub counts issues and pull requests in one sequence and gh issue view answers for either — taking it would branch off the base under its title instead of checking its head out (CreateWorktreeFromPR). The URL is the only field in the answer that tells the two apart.

And the reference itself never names the branch, resolved or not: git accepts #381 as a branch name, and most shells read the # as the start of a comment — git checkout #381 is git checkout. Caught in the headless smoke, not in review.

Almost all of it already existed

toBranchName() reused — the slug and the help line are #413's
CreateWorktree() reused — an issue's worktree is an ordinary branch off the chosen base
writeAtPrompt() reused — waits for Terminal.Ready, so the paste outlives the setup script (#447)
bracketedPaste() reused — why a multi-line body is text and not a run of Enters
project.Issue new — one gh issue view <n> --json number,title,body,url, ~60 lines
lib/issue.ts new — reference to number, issue to prompt text; pure, tested
ghFailures one row

No new screen, no route, no topbar entry, no dependency. The body is cut at 4 000 runes with the cut marked; the URL above it is the rest.

Testing

  • internal/project/issue_test.go — the gh call's shape, the pull request refusal, the body cut (boundary pinned as a literal and crossed by one), and the not-found wording pinned onto its message.
  • frontend/src/lib/issue.test.ts — 10 cases: #N, issue URLs with tails, enterprise hosts, and every shape that is not a reference; the brief's bracketing and its empty-body branch. One asserts toBranchName(issueName(issue)) end to end, so the branch this prints is the branch it makes.
  • Headless smoke against real GitHub (the node-only gate cannot render): the dialog driven over CDP through all six states above, no exceptions and no console errors. That is where the #-in-a-branch-name bug surfaced.
  • Local gate green: gofmt, go vet, go test ./..., biome check, tsc, vitest (1383), vite build, and the three-OS build/vet loop.

Provider symmetry

The handoff is writeAtPrompt + bracketed paste — the same mechanism the pull request handoff already ships on, provider-agnostic. No per-provider gap, so no new docs/ceilings.md bullet; the paste bullet there now names both callers.

https://claude.ai/code/session_01GSu6sRs6pBuZHmWaJmuZ3o

The New worktree dialog's name field took a branch name or the task in
plain words. It takes an issue too now — "#128", or the URL copied out of
the browser — looked up through gh on the project's own account.

What that buys is the other half: the session opened on that worktree
finds the issue at its prompt. Number, title, link and body, as one
bracketed paste so the body's newlines are text rather than a run of
Enters, and unsent — the Enter stays the user's, as with the pull request
handoff. Typing the context in by hand was the only way an agent learned
what its worktree was for.

Everything the flow needs already existed: toBranchName slugs the title
and prints the exact branch under the field before anything is created
(#413), CreateWorktree makes an ordinary branch off the chosen base,
writeAtPrompt waits out the setup script and the provider's boot (#447),
and bracketedPaste is why the paste is one block. What is new is one gh
call, one pure module, and a row in the gh message table.

Two refusals, both because the field is a branch name first:

- A bare number stays a branch name. "#" is the marker, so there is one
  field with one meaning rather than a mode to be in.
- A pull request's number is refused, naming the screen that does open
  pull requests. GitHub counts issues and pull requests in one sequence
  and `gh issue view` answers for either; taking it would branch off the
  base under its title instead of checking its head out. The URL is the
  only field in the answer that tells the two apart.

And the reference itself never names the branch, resolved or not: git
accepts "#381" as a branch name, and most shells read the "#" as the
start of a comment, so `git checkout #381` is `git checkout`.

Claude-Session: https://claude.ai/code/session_01GSu6sRs6pBuZHmWaJmuZ3o
Both sides changed the body of createWorktree: main's session fork queues
the parent's conversation before the setup script, this branch writes the
issue at the new session's prompt after it. Neither replaces the other —
a forked worktree named by an issue does both — so the resolution keeps
main's fork block and the prompt write beside it.

Claude-Session: https://claude.ai/code/session_01GSu6sRs6pBuZHmWaJmuZ3o
@omartelo
omartelo merged commit 4b9777c into main Sep 5, 2026
4 checks passed
@omartelo
omartelo deleted the feat/worktree-from-issue branch September 5, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant