Skip to content

Conversation

vito
Copy link
Contributor

@vito vito commented Jul 15, 2025

This does two things:

  • Enhances apply to preserve unstaged changes, through a delicate git dance:
    1. git diff to collect user's unstaged changes
    2. git stash create to make a backup of all changes (without affecting the stash list)
    3. git reset --hard to switch to a pristine state
    4. git merge --squash (no --autostash) to pull in the env changes
    5. git commit -m "temporary commit" (see later steps)
    6. git apply the patch from 1 (but this stages them)
    7. git reset to move the user's changes back to unstaged
    8. git reset --soft HEAD~1 to move the temporary commit into staging
  • Adds a new tool, environment_sync_from_user, which:
    1. git diff to collect the user's unstaged changes as a patch
    2. Applies the patch to the environment, currently using patch -p1
      • This is cheating a bit, but it's the second time that I wanted to use 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 as sh which we already depend on.
    3. Does a cu apply so that the user's working copy has the changes staged, ready for another sync

TODO

  • Some way to guarantee that multiple environments won't go haywire and sync from the user unnecessarily
  • Continuously sync to the user's worktree (duh) - should be easy ish now that we have a robust apply, as long as it's not fundamentally concurrent with user edits (is that even solveable...?)
  • Other safety concerns?
  • Right now this PR is based on my original file-patching PR, since I once again wanted to be able to use patch -p1 in the environment and it already had the pattern for it.
  • Handle files marked 'binary'
  • Handle files being deleted by the user:
ERROR: conflict detected when re-applying user changes:
Applied patch to 'tests/test_for_loops.dang' cleanly.
error: tests/test_for_loops_objects.dang: does not exist in index

vito added 5 commits July 15, 2025 17:18
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]>
@vito vito force-pushed the continuous-apply branch from 5f72b3d to bbecb79 Compare July 16, 2025 03:10
vito added 4 commits July 16, 2025 13:54
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]>
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]>
},
}

var EnvironmentEnableTrackingTool = &Tool{
Copy link
Contributor

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.

Copy link
Contributor Author

@vito vito Jul 16, 2025

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.

Copy link
Contributor Author

@vito vito Jul 16, 2025

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")
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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")
Copy link
Contributor

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.

Copy link
Contributor Author

@vito vito Jul 16, 2025

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

vito added 2 commits July 16, 2025 20:32
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]>
@vito vito force-pushed the continuous-apply branch from 85eb304 to 77a2330 Compare July 17, 2025 01:01
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