-
Notifications
You must be signed in to change notification settings - Fork 164
bidirectional syncing to/from user worktree #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
IMO it's OK to depend on the environment's `patch` command; it's just as universal as `sh` which we already depend on, and it's heavily used in various Linux distro packaging systems. Signed-off-by: Alex Suraci <[email protected]>
This change allows you to run `cu apply` continuously, by doing a somewhat delicate git dance: 1. `git diff` to save the unstaged changes to a .patch file 2. `git reset --hard` to get back to a pristine state 3. `git merge --squash` (no `--autostash`) to pull in the env changes 4. `git commit -m "temporary commit"` 5. `git apply` the patch from 1 (but this stages them) 6. `git reset` to move them back to unstaged 7. `git reset --soft HEAD~1` to move the temp commit into staging The spookiest part is probably 2, since that'll nuke any non-agent changes the user had staged. Maybe there's a safer way? Signed-off-by: Alex Suraci <[email protected]>
* new `environment_sync_from_user` tool: applies user's unstaged changes to the env, commits, syncs the env back to the user (bidirectional) * `git reset --hard` is now done after creating a refless stash, so the user can bring everything back if something goes wrong * add `Environment.ApplyPatch`, which currently depends on `patch` being available in the environment * clean up all the git code that wasn't using helpers Signed-off-by: Alex Suraci <[email protected]>
Signed-off-by: Alex Suraci <[email protected]>
I hit a scenario where Claude compacted the message history and then created a new environment. It tried to sync my local changes to 'recover' the progress, which is unfortunate since it's only meant to do that when the user explicitly requests it. So, this adds a layer of defensiveness: now when you enable tracking for a branch, we also track which environment the branch is tracking, and error early if there's a mismatch. Signed-off-by: Alex Suraci <[email protected]>
Signed-off-by: Alex Suraci <[email protected]>
Signed-off-by: Alex Suraci <[email protected]>
this supports the following flow: 1. made local edits, e.g. to write a failing test 2. create a tracking environment and tell it to sync my changes previously this would fail even though the patch is a no-op; now we'll use a 3-way merge, which accepts no-ops, and we'll --check first to avoid leaving conflict markers Signed-off-by: Alex Suraci <[email protected]>
mcpserver/tools.go
Outdated
}, | ||
} | ||
|
||
var EnvironmentEnableTrackingTool = &Tool{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't this be better via a CLI command? it makes sense to me that environment_sync_from_user needs to be a special tool that the user has to explicitly request - the agent needs to know that things have changed on disk- but this toggle feels like something that you may want to turn on and off independently of your agent session and the agent doesn't really need to know it's happening in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure yet. 🤔 The idea was to keep it all in the flow of using CC, so I don't have to tab over to a separate terminal every time I start on another task.
If (IF!) this gets reliable enough I think I wouldn't want to have to enable it every time. But I don't want to sacrifice the background envs functionality.
One intuitive model to me, in the spirit of being compatible with typical Claude Code usage, is:
- When you just give CC a prompt like normal, it creates an environment that tracks your current branch.
- When you prompt something like "on branch foo-bar" it'll create that branch and configure it to track the environment. From then on it'll continuously amend a commit on that branch. Eventually you can just checkout the branch and push.
For me that would let container-use
become purely additive; I wouldn't even need to use the CLI.
Not totally sure what happens when you checkout a background branch while the agent is doing work there though. I guess implicitly the commit would be reset --soft
'd.
Also, related but more tangential, I'd want these new environments to bring along any staged/unstaged changes (but maybe not uncommitted files?) on creation. I felt this pain today when I wrote a failing test and wanted the agent to fix it. The newly created env didn't have my failing test, so the agent ran them and they passed and the agent got confused.
Anyway, I get this is all swimming upstream compared to the current model, but it seems like it would significantly lower the mental overhead, even with parallel tasks, so seems worth exploring a bit. Still some gaps in this scheme to figure out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you just give CC a prompt like normal, it creates an environment that tracks your current branch.
Ah dang, one immediate problem with this idea is the sub-environments that get created for sub-tasks end up becoming the tracked environment. 🤔
Hmm, hmm, hmm...
edit: added a required ephemeral: bool
arg and it seems to respect it
} | ||
|
||
func (r *Repository) TrackedEnvironment(ctx context.Context, branch string) (string, error) { | ||
envID, err := RunGitCommand(ctx, r.userRepoPath, "config", "get", "--default=", "branch."+branch+".environment") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting, storing this in gitconfig... eventually it'll result in a long list of kv pairs, right? does --default=
put it in repo-level gitconfig?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It goes into .git/config
, and as long as you're just repointing e.g. main
at new environments over time it just replaces the existing field. The rest of them go away when you delete the branch, so this doesn't seem to grow forever.
defer func() { | ||
if rerr != nil { | ||
fmt.Fprintf(w, "ERROR: %s\n", rerr) | ||
fmt.Fprintf(w, "Your prior changes can be restored with `git stash apply %s`\n", stashID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting... i wonder if there are some cases this can (should?) be done automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i also suspect there are some failures cases where this stash apply instruction will fail, no? i think you've gotta combine it with a reset --hard HEAD
for the errors before the the temporary commit and a reset --hard head~
for the errors where the temporary commit exists... overall a big faff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it should probably be done automatically.
hasUnstagedChanges := len(diffOutput) > 0 | ||
|
||
fmt.Fprintf(w, "Creating virtual stash as backup...\n") | ||
stashID, err := RunGitCommand(ctx, r.userRepoPath, "stash", "create") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this stash stay stashed after a fully successful apply? it strikes me that on success, the user doesn't have any obvious way to restore the pre-apply unstaged files state and they're gonna be all mixed up with the agent's changes in the staging area.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this clears things up: git stash create
doesn't actually change the working state - it just records it into an anonymous commit so you can bring it back, while leaving the staged/unstaged changes there. So it's different from regular git stash
which also cleans up the working state.
The flow is:
- When the env syncs changes to the branch, it only syncs to the staging area; the user's unstaged changes stay unstaged.
- When the env syncs changes from the user, the unstaged changes get applied to the env, and then synced back to the branch, so the unstaged changes become staged (maintaining the rule of "staged = env, unstaged = local").
this is still not robust enough to cover the scenario where the user deleted a file. we should just do a hard reset to the environment's changes instead, and not attempt to restore their changes, since we just synced them over. This reverts commit 856d418.
we literally just migrated their unstaged changes to the env, so this should be equivalent and much more foolproof Signed-off-by: Alex Suraci <[email protected]>
Signed-off-by: Alex Suraci <[email protected]>
Signed-off-by: Alex Suraci <[email protected]>
This does two things:
apply
to preserve unstaged changes, through a delicategit
dance:git diff
to collect user's unstaged changesgit stash create
to make a backup of all changes (without affecting the stash list)git reset --hard
to switch to a pristine stategit merge --squash
(no--autostash
) to pull in the env changesgit commit -m "temporary commit"
(see later steps)git apply
the patch from 1 (but this stages them)git reset
to move the user's changes back to unstagedgit reset --soft HEAD~1
to move the temporary commit into stagingenvironment_sync_from_user
, which:git diff
to collect the user's unstaged changes as a patchpatch -p1
patch
in the environment, IMO it's worth just requiring it in the environment as it's an incredibly common tool with an interface that's just as stable assh
which we already depend on.cu apply
so that the user's working copy has the changes staged, ready for another syncTODO
apply
, as long as it's not fundamentally concurrent with user edits (is that even solveable...?)patch -p1
in the environment and it already had the pattern for it.