Skip to content

feat(worktree): resume into a preserved worktree on re-invocation#76

Open
pro-vi wants to merge 2 commits intokunchenguid:mainfrom
pro-vi:feat/worktree-resume
Open

feat(worktree): resume into a preserved worktree on re-invocation#76
pro-vi wants to merge 2 commits intokunchenguid:mainfrom
pro-vi:feat/worktree-resume

Conversation

@pro-vi
Copy link
Copy Markdown

@pro-vi pro-vi commented Apr 18, 2026

Problem

When gnhf preserves a worktree on exit (any run that lands a commit), subsequent invocations with the same prompt fail:

gnhf: Command failed: git worktree add -b 'gnhf/...' '.../gnhf-worktrees/...'
Preparing worktree (new branch 'gnhf/...')
fatal: a branch named 'gnhf/...' already exists

The branch is still registered against the preserved worktree, so git worktree add -b cannot re-create it. The user's only options today are to merge the branch and remove the worktree, or delete the branch manually. There's no way to resume and keep iterating on the same preserved worktree.

Change

Detect the "worktree already preserved" case and resume the existing run instead of re-creating.

  • New worktreeExists(baseCwd, worktreePath) in src/core/git.ts reads git worktree list --porcelain and returns true when the path is already registered.
  • initializeWorktreeRun in src/cli.ts checks for the preserved worktree plus the matching .gnhf/runs/<runId>/ directory. When both are present it calls resumeRun rather than createWorktree + setupRun, and returns a resumed: true flag.
  • The call site reads startIteration from the preserved run via getLastIterationNumber and skips installing the worktreeCleanup handler on the resume path, so Ctrl-C on a resumed invocation cannot take down a worktree that already carries earlier iterations' commits.

The non-worktree resume path (existing behavior when running gnhf from an already-checked-out gnhf/* branch) is untouched.

Example

$ cat prompt.md | gnhf --worktree --agent claude
... run 1 lands 3 commits ...
  gnhf: worktree preserved at /path/gnhf-worktrees/xyz

$ cat prompt.md | gnhf --worktree --agent claude
  gnhf: resuming preserved worktree at /path/gnhf-worktrees/xyz
  gnhf: continuing run xyz from iteration 3
... run 2 appends commits on the same branch ...

Test plan

  • npm run typecheck
  • npm run lint
  • npm run format:check
  • npm test (354/354 passing, including new e2e)
  • npm run test:e2e (7/7 passing)

New e2e test runs --worktree twice with the same prompt and asserts: the second invocation does not mention "already exists", logs "resuming preserved worktree", lands an additional commit on the same branch, and continues iteration numbering (gnhf #2:).

Compatibility

  • First invocation behavior unchanged (branch does not exist yet, falls through to current createWorktree + setupRun path).
  • Second invocation with a different prompt still spawns a fresh worktree under a different slug, same as before.
  • If the worktree directory exists on disk but is not registered as a git worktree (e.g. removed from git but left on disk), worktreeExists returns false and the current path runs, matching existing behavior as closely as possible.

When gnhf preserves a worktree on exit (any run that lands a commit),
subsequent invocations with the same prompt currently fail in
`git worktree add -b` with "a branch named 'gnhf/...' already exists"
because the branch is still registered against the preserved worktree.

Detect that case and resume the existing run instead of re-creating:
- New `worktreeExists(baseCwd, worktreePath)` helper reads
  `git worktree list --porcelain` and returns true when the path is
  already registered.
- `initializeWorktreeRun` checks for the preserved worktree plus the
  matching `.gnhf/runs/<runId>/` directory, and when both are present
  calls `resumeRun` instead of `createWorktree` + `setupRun`.
- On the resume path the call site reads `startIteration` from the
  preserved run and skips installing the worktree-cleanup handler, so
  Ctrl-C on a resumed invocation cannot take down a preserved worktree
  carrying earlier iterations' commits.

Adds an e2e test that runs `--worktree` twice with the same prompt and
asserts the second invocation (a) does not mention "already exists",
(b) logs "resuming preserved worktree", (c) lands an additional commit
on the same branch, and (d) continues iteration numbering (gnhf kunchenguid#2:).
Copy link
Copy Markdown

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

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: 9a130f76d2

ℹ️ 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/core/git.ts Outdated
Comment thread src/cli.ts
P1 (worktree path normalization): `worktreeExists` compared raw strings
from `git worktree list --porcelain` against `path.join` output, which
could miss real matches on Windows where git emits forward slashes and
path.join uses backslashes. Resolve both sides with `path.resolve`
before comparing so the lookup is platform-agnostic. Unit tests in
git.test.ts cover the match, non-match, normalize, and command-failure
paths.

P2 (branch verification before resume): if a user manually switched the
preserved worktree to a different branch (or detached HEAD) between
invocations, the previous resume path would run the orchestrator
against the wrong ref and silently write new commits there. Read the
worktree's current branch before resuming and throw a clear error when
it does not match the expected `gnhf/<runId>`, pointing at the commands
the user can run to restore or remove the worktree. An e2e test creates
a preserved worktree, switches it to a sideways branch, and asserts the
second invocation fails with the branch name in the error message.
@pro-vi
Copy link
Copy Markdown
Author

pro-vi commented Apr 18, 2026

Thanks for the review. Both items addressed in e3625a7:

P1 (path normalization): worktreeExists now calls path.resolve on both the git worktree list --porcelain output and the target path before comparing, so Windows slash/backslash mismatches no longer miss a real match. Added unit tests for the match, non-match, normalize (trailing slash + .. segment), and command-failure paths.

P2 (branch verification): initializeWorktreeRun now reads the preserved worktree's current branch before resuming and throws a clear error if it is not gnhf/<runId>. The message points at the exact git -C / git worktree remove commands the user can run to resolve. Added an e2e test that switches the preserved worktree to a sideways branch between invocations and asserts the second run fails with the branch name in the error.

All 359 tests + lint + typecheck + prettier remain clean.

@kunchenguid
Copy link
Copy Markdown
Owner

thanks for the solid work here, especially the P1/P2 follow-ups. reviewed the diff and ran everything locally - all green (363 tests, lint, typecheck, format:check).

ready to merge once the conflict with main is sorted. it's a trivial one: #77 added closeSync / createReadStream / createWriteStream to the same fs import block in src/cli.ts where this PR adds existsSync. a merge or rebase against main should resolve it cleanly.

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.

2 participants