Skip to content

Add branch command for AI-suggested branch names from local changes - #29

Merged
rafaeelricco merged 7 commits into
mainfrom
ai-branch-creator
May 26, 2026
Merged

Add branch command for AI-suggested branch names from local changes#29
rafaeelricco merged 7 commits into
mainfrom
ai-branch-creator

Conversation

@rafaeelricco

Copy link
Copy Markdown
Owner

Summary

  • Add commit branch (alias new-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 with git switch -c.
  • Introduce getLocalChangeContext and createAndSwitchBranch git helpers, decoder-based branch suggestion parsing/validation in src/domain/branch/suggestions.ts, and getBranchNamePrompt alongside existing commit prompts.
  • Wire generateBranchNameSuggestions through the LLM router with typed BranchNameSuggestions results lifted from Result to Future.
  • Register the command in the CLI parser, help text, update notifier set, and entrypoint; cover CLI, domain, parser, and git integration paths with tests.

Test plan

  • Run pnpm test and confirm branch/parser/suggestions/repo integration tests pass.
  • With local uncommitted changes, run commit branch and verify three grounded slug options appear and selecting one switches HEAD.
  • With a clean working tree, run commit branch and verify it stops before calling the LLM with a clear no-changes message.
  • Confirm commit new-branch resolves to the same command as commit branch.

- 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.
chatgpt-codex-connector[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@rafaeelricco rafaeelricco self-assigned this May 26, 2026
- 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Open in Web View Automation 

Sent by Cursor Automation: Find critical bugs

Comment thread src/infra/git/repo.ts Outdated
- 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`.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/infra/git/repo.ts Outdated
Comment thread src/cli/branch.ts Outdated
Comment thread src/infra/git/repo.ts Outdated
Comment thread src/infra/git/repo.ts Outdated
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.
@rafaeelricco
rafaeelricco merged commit 2e4c459 into main May 26, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/infra/git/repo.ts
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) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +76 to +77
const firstNl = t.indexOf("\n");
const body = firstNl === -1 ? "" : t.slice(firstNl + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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