Merge pull request #3205: feat(eval): isolated/apply/merge options for agent() helper

Resolves conflict in test/task/worktree.test.ts by keeping both the
getRepoRoot (main) and applyNestedPatches (PR) describe blocks.

Extends the PR's Python/JS work to the remaining workflow runtimes:
- eval/rb/prelude.rb, eval/jl/prelude.jl: agent() now accepts and
  forwards isolated/apply/merge (as booleans) plus returnHandle, and the
  return_handle node carries isolated/patch_path/branch_name/
  nested_patches/changes_applied/isolation_summary.

Post-merge fixups:
- task/index.ts: drop dead commitStyle var (the dedup refactor reads
  task.isolation.commits inside makeIsolationCommitMessage).
- CHANGELOG: move the misplaced Added entry under [Unreleased], correct
  the stale "defaults track task.isolation.mode" wording to the final
  strict opt-in behavior, and note all four runtimes.

Fixes #3196
This commit is contained in:
can1357
2026-06-22 21:56:45 +02:00
15 changed files with 1511 additions and 292 deletions
+34 -8
View File
@@ -194,6 +194,14 @@ export async function captureDeltaPatch(isolationDir: string, baseline: Worktree
/**
* Apply nested repo patches directly to their working directories after parent merge.
*
* Pre-existing dirty state in a nested repo is stashed before the patch is
* applied and popped back after the commit, so unrelated user edits never get
* folded into the agent's commit. A failing `git stash pop` (e.g. user edits
* collide with the patched lines) leaves the stash entry intact and emits a
* `logger.warn` — the caller's catch handler turns the broader nested-apply
* failure into a non-fatal system notification.
*
* @param commitMessage Optional async function to generate a commit message from the combined diff.
* If omitted or returns null, falls back to a generic message.
*/
@@ -220,15 +228,33 @@ export async function applyNestedPatches(
}
const combinedDiff = repoPatches.map(p => p.patch).join("\n");
for (const { patch } of repoPatches) {
await git.patch.applyText(nestedDir, patch);
}
// Commit so nested repo history reflects the task changes
if ((await git.status(nestedDir)).trim().length > 0) {
const msg = (await commitMessage?.(combinedDiff)) ?? "changes from isolated task(s)";
await git.stage.files(nestedDir);
await git.commit(nestedDir, msg);
// Preserve any pre-existing dirty state (tracked + untracked) so we
// commit only the agent delta, not the user's in-flight work.
const stashed =
(await git.status(nestedDir)).trim().length > 0
? await git.stash.push(nestedDir, `omp-isolation-${Snowflake.next()}`)
: false;
try {
for (const { patch } of repoPatches) {
await git.patch.applyText(nestedDir, patch);
}
if ((await git.status(nestedDir)).trim().length > 0) {
const msg = (await commitMessage?.(combinedDiff)) ?? "changes from isolated task(s)";
await git.stage.files(nestedDir);
await git.commit(nestedDir, msg);
}
} finally {
if (stashed) {
try {
await git.stash.pop(nestedDir, { index: true });
} catch (popErr) {
logger.warn("Pre-existing nested-repo dirty state could not be auto-restored", {
nestedDir,
error: popErr instanceof Error ? popErr.message : String(popErr),
});
}
}
}
}
}