diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index 7ef6140bc..7c55527c8 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -112,7 +112,7 @@ An agent-specific entry wins over `default`. `claude`, `codex`, and `opencode` a teamai workflow start build --driver local ``` -`--driver local` runs one step at a time in the current directory. Each attempt uses `result.json` under the scope's TeamAI data directory, outside the checkout. Prompt steps are read-only: if a tool changes the repository, the step fails and leaves those files in place for inspection. The driver calls the same `next` and `report` operations shown below: +`--driver local` runs one step at a time in the current directory. Each attempt uses `result.json` under the scope's TeamAI data directory, outside the checkout. Prompt steps are read-only: if a tool changes the repository, the step fails and leaves those files in place for inspection. This includes changes to files inside an existing untracked directory; leaving that directory unchanged does not fail the step. The driver calls the same `next` and `report` operations shown below: ```text teamai workflow start build diff --git a/docs/guide/zh-CN/workflow.md b/docs/guide/zh-CN/workflow.md index f20ef171f..a90e401e9 100644 --- a/docs/guide/zh-CN/workflow.md +++ b/docs/guide/zh-CN/workflow.md @@ -112,7 +112,7 @@ workflows: teamai workflow start build --driver local ``` -`--driver local` 在当前目录里一次运行一个步骤。每次 attempt 的 `result.json` 位于当前 scope 的 TeamAI 数据目录中,不在产品检出目录内。提示词步骤只读;如果工具修改了仓库,步骤会失败,同时保留文件供检查。驱动调用的 `next` 和 `report` 与下面相同: +`--driver local` 在当前目录里一次运行一个步骤。每次 attempt 的 `result.json` 位于当前 scope 的 TeamAI 数据目录中,不在产品检出目录内。提示词步骤只读;如果工具修改了仓库,步骤会失败,同时保留文件供检查。这也包括已有未跟踪目录内的文件变化;该目录保持不变时,步骤不会因此失败。驱动调用的 `next` 和 `report` 与下面相同: ```text teamai workflow start build diff --git a/src/__tests__/workflow-local-driver.test.ts b/src/__tests__/workflow-local-driver.test.ts index 2f03301e5..4cd07d824 100644 --- a/src/__tests__/workflow-local-driver.test.ts +++ b/src/__tests__/workflow-local-driver.test.ts @@ -234,6 +234,44 @@ Do not use this copy. }); }); + it.each(['edit', 'add', 'remove'] as const)('detects a file change (%s) inside an existing untracked directory', async (change) => { + const { config, workspace, runId } = await fixture(); + const directory = path.join(workspace, 'notes'); + fs.mkdirSync(directory); + const existing = path.join(directory, 'draft.txt'); + fs.writeFileSync(existing, 'before\n'); + fs.writeFileSync(path.join(directory, 'keep.txt'), 'unchanged\n'); + const changed = change === 'add' ? path.join(directory, 'new.txt') : existing; + const adapter: LocalToolAdapter = { + run: vi.fn(async () => { + if (change === 'remove') fs.rmSync(changed); + else fs.writeFileSync(changed, 'after\n'); + return { exitCode: 0, output: { ok: true } }; + }), + }; + + const done = await runLocalDriver(config, runId, workspace, { adapter, toolPaths }); + + expect(done).toMatchObject({ data: { done: { outcome: 'failed' } } }); + expect(await readRun(config, runId)).toMatchObject({ + steps: [{ status: 'failed', failure: { code: 'read_only_dirty', message: expect.stringContaining(`notes/${path.basename(changed)}`) } }], + }); + if (change === 'remove') expect(fs.existsSync(changed)).toBe(false); + else expect(fs.readFileSync(changed, 'utf8')).toBe('after\n'); + }); + + it('allows an unchanged untracked directory', async () => { + const { config, workspace, runId } = await fixture(); + fs.mkdirSync(path.join(workspace, 'notes')); + fs.writeFileSync(path.join(workspace, 'notes', 'draft.txt'), 'before\n'); + const adapter: LocalToolAdapter = { + run: vi.fn(async () => ({ exitCode: 0, output: { ok: true } })), + }; + + expect(await runLocalDriver(config, runId, workspace, { adapter, toolPaths })) + .toMatchObject({ data: { done: { outcome: 'succeeded' } } }); + }); + it('detects when a tool removes a path that was dirty before the step', async () => { const { config, workspace, runId } = await fixture(); const dirty = path.join(workspace, 'dirty-before.txt'); diff --git a/src/workflow/local-driver.ts b/src/workflow/local-driver.ts index 3c6995b6f..35946f6d1 100644 --- a/src/workflow/local-driver.ts +++ b/src/workflow/local-driver.ts @@ -117,7 +117,7 @@ interface WorktreeSnapshot { function gitStatus(workspace: string): Promise<{ ok: boolean; text: string }> { return new Promise((resolve) => { - execFile('git', ['status', '--porcelain'], { cwd: workspace, env: process.env }, (error, stdout, stderr) => { + execFile('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: workspace, env: process.env }, (error, stdout, stderr) => { resolve(error ? { ok: false, text: stderr.trim() } : { ok: true, text: stdout }); }); });