diff --git a/apps/server/agent/cleanup-manager.ts b/apps/server/agent/cleanup-manager.ts index f93c5c4..e35d406 100644 --- a/apps/server/agent/cleanup-manager.ts +++ b/apps/server/agent/cleanup-manager.ts @@ -222,7 +222,7 @@ export class CleanupManager { * 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. */ - async getDirtyWorktreePaths(alias: string, initiativeId: string | null): Promise { + async getDirtyWorktreePaths(alias: string, initiativeId: string | null): Promise<{ name: string; absPath: string }[]> { const agentWorkdir = this.getAgentWorkdir(alias); try { @@ -242,13 +242,13 @@ export class CleanupManager { worktreePaths.push({ absPath: join(agentWorkdir, 'workspace'), name: 'workspace' }); } - const dirty: string[] = []; + const dirty: { name: string; absPath: string }[] = []; for (const { absPath, name } of worktreePaths) { try { 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 { - dirty.push(name); + dirty.push({ name, absPath }); } } return dirty; diff --git a/apps/server/agent/manager.ts b/apps/server/agent/manager.ts index dde6b9b..3bde16a 100644 --- a/apps/server/agent/manager.ts +++ b/apps/server/agent/manager.ts @@ -453,12 +453,16 @@ export class MultiProviderAgentManager implements AgentManager { const dirtyPaths = await this.cleanupManager.getDirtyWorktreePaths(agent.name, agent.initiativeId); 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 = - 'You have uncommitted changes in the following project directories:\n' + + 'You have uncommitted changes in the following directories:\n' + dirtyList + '\n\n' + - 'For each directory listed above, `cd` into it, then run `git add -A && git commit -m ""` ' + - 'with an appropriate commit message describing the work. Do not make any other changes.'; + 'For each directory listed above, `cd` into the EXACT absolute path shown, then run:\n' + + '1. `git add -u` to stage only tracked modified files\n' + + '2. `git commit -m ""` 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 }); diff --git a/docs/agent.md b/docs/agent.md index 9e1cf8e..560ca1a 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -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: -1. `CleanupManager.getDirtyWorktreePaths()` runs `git status --porcelain` in each project subdirectory (not the parent `agent-workdirs//` dir) +1. `CleanupManager.getDirtyWorktreePaths()` runs `git status --porcelain` in each project subdirectory (not the parent `agent-workdirs//` dir), returns `{ name, absPath }[]` 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/\``) -4. The agent `cd`s into each listed directory and commits +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 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 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).