fix: Use absolute paths and git add -u in post-completion commit resume

Prevents two bugs in the resumeForCommit flow:
1. Agent navigated to main repo instead of worktree due to relative paths
   in commit prompt — now uses absolute paths from getDirtyWorktreePaths
2. git add -A staged unrelated files (screenshots, other agents' work) —
   now uses git add -u to only stage tracked modified files
This commit is contained in:
Lukas May
2026-03-05 17:13:31 +01:00
parent 8804455c77
commit f3042abe04
3 changed files with 15 additions and 11 deletions

View File

@@ -222,7 +222,7 @@ export class CleanupManager {
* Get the relative subdirectory names of dirty worktrees for an agent. * Get the relative subdirectory names of dirty worktrees for an agent.
* Returns an empty array if all worktrees are clean or the workdir doesn't exist. * Returns an empty array if all worktrees are clean or the workdir doesn't exist.
*/ */
async getDirtyWorktreePaths(alias: string, initiativeId: string | null): Promise<string[]> { async getDirtyWorktreePaths(alias: string, initiativeId: string | null): Promise<{ name: string; absPath: string }[]> {
const agentWorkdir = this.getAgentWorkdir(alias); const agentWorkdir = this.getAgentWorkdir(alias);
try { try {
@@ -242,13 +242,13 @@ export class CleanupManager {
worktreePaths.push({ absPath: join(agentWorkdir, 'workspace'), name: 'workspace' }); worktreePaths.push({ absPath: join(agentWorkdir, 'workspace'), name: 'workspace' });
} }
const dirty: string[] = []; const dirty: { name: string; absPath: string }[] = [];
for (const { absPath, name } of worktreePaths) { for (const { absPath, name } of worktreePaths) {
try { try {
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: absPath }); const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: absPath });
if (stdout.trim().length > 0) dirty.push(name); if (stdout.trim().length > 0) dirty.push({ name, absPath });
} catch { } catch {
dirty.push(name); dirty.push({ name, absPath });
} }
} }
return dirty; return dirty;

View File

@@ -453,12 +453,16 @@ export class MultiProviderAgentManager implements AgentManager {
const dirtyPaths = await this.cleanupManager.getDirtyWorktreePaths(agent.name, agent.initiativeId); const dirtyPaths = await this.cleanupManager.getDirtyWorktreePaths(agent.name, agent.initiativeId);
if (dirtyPaths.length === 0) return false; if (dirtyPaths.length === 0) return false;
const dirtyList = dirtyPaths.map(p => `- \`${p}/\``).join('\n'); // Use absolute paths so the agent can't accidentally commit in the main repo
// Use `git add -u` (tracked files only) instead of `git add -A` to avoid staging unrelated files
const dirtyList = dirtyPaths.map(p => `- \`${p.absPath}\``).join('\n');
const commitPrompt = const commitPrompt =
'You have uncommitted changes in the following project directories:\n' + 'You have uncommitted changes in the following directories:\n' +
dirtyList + '\n\n' + dirtyList + '\n\n' +
'For each directory listed above, `cd` into it, then run `git add -A && git commit -m "<message>"` ' + 'For each directory listed above, `cd` into the EXACT absolute path shown, then run:\n' +
'with an appropriate commit message describing the work. Do not make any other changes.'; '1. `git add -u` to stage only tracked modified files\n' +
'2. `git commit -m "<message>"` with a message describing the work\n' +
'Do not use `git add -A` or `git add .`. Do not stage untracked files. Do not make any other changes.';
await this.repository.update(agentId, { status: 'running', pendingQuestions: null, result: null }); await this.repository.update(agentId, { status: 'running', pendingQuestions: null, result: null });

View File

@@ -138,10 +138,10 @@ When Agent A asks Agent B a question via `cw ask` and Agent B is idle, the conve
After an agent completes (status → `idle`), `tryAutoCleanup` checks if its project worktrees have uncommitted changes: After an agent completes (status → `idle`), `tryAutoCleanup` checks if its project worktrees have uncommitted changes:
1. `CleanupManager.getDirtyWorktreePaths()` runs `git status --porcelain` in each project subdirectory (not the parent `agent-workdirs/<alias>/` dir) 1. `CleanupManager.getDirtyWorktreePaths()` runs `git status --porcelain` in each project subdirectory (not the parent `agent-workdirs/<alias>/` dir), returns `{ name, absPath }[]`
2. If all clean → worktrees and logs removed immediately 2. If all clean → worktrees and logs removed immediately
3. If dirty → `resumeForCommit()` resumes the agent's session with a prompt listing the specific dirty subdirectories (e.g. `- \`my-project/\``) 3. If dirty → `resumeForCommit()` resumes the agent's session with a prompt listing **absolute paths** to dirty subdirectories, using `git add -u` (tracked files only) to avoid staging unrelated files
4. The agent `cd`s into each listed directory and commits 4. The agent `cd`s into each listed absolute path and commits tracked changes only
5. On next completion, cleanup runs again. `MAX_COMMIT_RETRIES` (1) limits retries — after that the workdir is left in place with a warning 5. On next completion, cleanup runs again. `MAX_COMMIT_RETRIES` (1) limits retries — after that the workdir is left in place with a warning
The retry counter is cleaned up on: successful removal, max retries exceeded, or unexpected error. It is **not** cleaned up when a commit retry is successfully launched (so the counter persists across the retry cycle). The retry counter is cleaned up on: successful removal, max retries exceeded, or unexpected error. It is **not** cleaned up when a commit retry is successfully launched (so the counter persists across the retry cycle).