Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/zh-CN/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions src/__tests__/workflow-local-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/workflow/local-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
Expand Down
Loading