Add branch command for AI-suggested branch names from local changes - #29
Conversation
- Introduce `branch` (alias `new-branch`) CLI command that reads local diff and status, asks the LLM for three slug suggestions, and switches to the chosen branch. - Add `src/domain/branch/suggestions.ts` with JSON fence stripping, decoder-based parsing, and `validateGitBranchName` enforcing kebab-case, length, trunk-name, and type-prefix rules. - Add `getBranchNamePrompt` in `src/domain/commit/prompts.ts` with a strict machine contract and grounding rules for slug generation. - Extend the LLM router with `generateBranchNameSuggestions` that pipes provider output through parse-and-validate into a typed `BranchNameSuggestions` result. - Add `getLocalChangeContext` and `createAndSwitchBranch` helpers in `src/infra/git/repo.ts` and wire the new command through the parser, help text, and update notifier set. - Cover the new flow with unit tests for the CLI, suggestion parsing and validation, parser routing, and git integration tests using an extended `createTempGitRepo` `unstaged` option.
- Render a `Branched` note after creation showing branch, base, and request metadata via new `renderBranchNote` helper. - Read untracked file bodies into the local change context with per-file and total byte caps so suggestions are grounded in new files. - Export `NO_LOCAL_CHANGES_MESSAGE` and `isNoLocalChangesError` from the git repo module, and handle that error in the branch CLI with a friendly outro instead of failing. - Refactor the branch name prompt to use a `work_snapshot` framing, tighten output format rules, and extend forbidden vocabulary to keep slugs grounded in the change. - Extend `createTempGitRepo` with an `untrackedFile` option and add integration and unit tests covering untracked context, no-changes outro, and branch note rendering variants.
- Add `confirmForkFromBase` to warn and prompt the user when the current branch differs from the detected base branch. - Abort branch creation with an "Operation cancelled." outro when the user declines or dismisses the confirmation. - Skip the confirmation when already on the base branch or when the base branch cannot be determined. - Extend `Branch.run` tests to cover acceptance, decline, cancellation, and unknown-base scenarios.
There was a problem hiding this comment.
Found and fixed one high-severity issue.
Bug and impact: commit branch included untracked file bodies in the AI prompt. A developer with an untracked .npmrc, credentials.json, SSH key, or similar local secret would send that content to the configured provider by asking for a branch name.
Root cause: getLocalChangeContext() parsed untracked paths from git status --porcelain, read each file from disk, appended up to 256 KB of contents, and the branch flow passed that context into generateBranchNameSuggestions().
Fix: I pushed be84ccc on rafaeelricco/critical-correctness-bugs-19e9. The fix keeps tracked diffs and porcelain status paths, removes untracked body reads, updates the branch prompt, and adds an integration test that proves an untracked credentials.json token stays out of the AI context.
Validation: pnpm vitest run test/infra/git/repo.integration.test.ts, pnpm vitest run test/cli/branch.test.ts test/domain/branch/suggestions.test.ts, pnpm typecheck, pnpm test, and pnpm lint all pass.
Sent by Cursor Automation: Find critical bugs
- Extend branch suggestions schema with `rationale` field and decode via `BranchSuggestion`, surfacing it in the `p.select` labels. - Expand forbidden first-segment list with vague verbs (`add`, `update`, `change`, etc.) and add tests covering rejection and refactor-suffix acceptance. - Rewrite `getBranchNamePrompt` with diversity axes, synthesis protocol, area-prefix guidance, and richer examples producing name/rationale objects. - Add `withTransientRetry` in `src/domain/llm/retry.ts` with exponential backoff, transient-error detection, and interactive retry prompt; wrap `generateCommitMessage`, `refineCommitMessage`, and `generateBranchNameSuggestions`. - Update router types and tests, and add unit tests for `isTransientLlmError` and `withTransientRetry`.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c49ec831b8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The previous getLocalChangeContext appended untracked file contents to the LLM prompt. That leaks local secrets (.npmrc, credentials.json, SSH keys) and follows symlinks via readFile, so an untracked symlink to $HOME could exfiltrate arbitrary files. Quoted porcelain paths were also silently skipped, and the porcelain call collapsed new directories to '?? dir/'. Remove the body-read path entirely. Pass --untracked-files=all so paths inside new directories are enumerated individually in the porcelain section; the LLM still has per-file visibility, just never file contents. Integration tests assert the path appears, the secret does not, and the file inside a new untracked directory is listed.
Previously, cancelling the branch picker (Ctrl-C on p.select) caused promptPick to throw, which mapRej then logged as a red error and propagated as exit code 1. Cancellation should be a quiet no-op. promptPick now returns Maybe<string>; run() short-circuits on Nothing via maybe(), mirroring the promptAdjustment pattern in src/cli/commit.ts. The "Operation cancelled." outro still fires; the red error log does not.
Collapses the maybePicked.maybe(...) call onto one line to match repo prettier config (printWidth). Local typecheck/lint/test/build all pass; CI's format check failed on the previous push.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d7386f771
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const isNoLocalChangesError = (err: unknown): err is Error => err instanceof Error && err.message === NO_LOCAL_CHANGES_MESSAGE; | ||
|
|
||
| const getLocalChangeContext = (): Future<Error, string> => | ||
| execGitChecked(["diff", "HEAD"], "Failed to read local diff").chain((diffStdout) => |
There was a problem hiding this comment.
Handle unborn HEAD when reading local change context
getLocalChangeContext always starts with git diff HEAD, which exits with an error in repositories that have no initial commit yet (HEAD is unborn). In that state, users can still have untracked/staged work, but the command fails before checking git status, so commit branch cannot suggest names for valid new-repo workflows.
Useful? React with 👍 / 👎.
| const firstNl = t.indexOf("\n"); | ||
| const body = firstNl === -1 ? "" : t.slice(firstNl + 1); |
There was a problem hiding this comment.
Parse single-line fenced JSON suggestions
The fence stripper assumes a newline after the opening backticks; when the model returns a single-line fenced payload like json {"suggestions":[...]} , firstNl is -1 and the function returns an empty body, causing valid JSON output to be rejected and branch suggestion generation to fail intermittently.
Useful? React with 👍 / 👎.


Summary
commit branch(aliasnew-branch) to read local git diff and status, ask the configured LLM for three prefix-free kebab-case slug suggestions, let you pick one, and create the branch withgit switch -c.getLocalChangeContextandcreateAndSwitchBranchgit helpers, decoder-based branch suggestion parsing/validation insrc/domain/branch/suggestions.ts, andgetBranchNamePromptalongside existing commit prompts.generateBranchNameSuggestionsthrough the LLM router with typedBranchNameSuggestionsresults lifted fromResulttoFuture.Test plan
pnpm testand confirm branch/parser/suggestions/repo integration tests pass.commit branchand verify three grounded slug options appear and selecting one switches HEAD.commit branchand verify it stops before calling the LLM with a clear no-changes message.commit new-branchresolves to the same command ascommit branch.