feat(models+gate): 模型按项目可配置(默认 opus-4.8) + diff 声明外文件硬闸
① 模型可配置(项目维度,默认 opus-4.8)
- 新增 projects.models(JSON,按角色 executor/planner/reviewer/conflict
覆盖,值可为字符串=全复杂度统一 或 {easy,medium,hard} 分档)
- models.ts 重构 resolveModel:优先级 项目级 models > 旧 project.model
(仅 executor/planner) > env > 默认 DEFAULT_MODEL(opus-4.8)
- 取消内置 fable/sonnet 分档默认:所有角色默认 opus-4.8(彻底回避 fable-5
不可用问题,需要时项目级显式配置即可);回退链改 opus→sonnet→fable
- API PATCH /projects 透传 models;sanitizeModels 落库校验
② diff 声明外文件闸(task.scopeFiles)
- 新增 tasks.scope_files(JSON glob/路径数组)
- checks.ts: globToRegExp/matchesAnyGlob + scopeFileGate(改动文件越界=硬闸,
空声明跳过,git 出错不拦截);pipeline runApproveGates 接入
- planner 拆解新增每子任务 files 字段:prompt 要求 + parseDecompose 解析 +
ingest 落 scopeFiles,自动填充声明范围
- executor prompt 注入「声明文件范围约束」,让 agent 知边界(gate 才公平)
迁移:projects.models / tasks.scope_files 走 ensureColumn 幂等迁移(旧库补列)
测试:models 默认/配置/优先级、scope glob/gate、planner files 解析、
store 持久化往返、迁移补列 —— 237 通过
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,47 @@ test('project + task creation, complexity routes initial status', () => {
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('patchProject models:持久化 + 读回(字符串/对象形式,非法值丢弃,null 清空)', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'mdl', repoPath: '/tmp/mdl-' + Math.random() });
|
||||
assert.equal(p.models, null);
|
||||
|
||||
const up = s.patchProject(p.id, {
|
||||
models: {
|
||||
executor: { easy: 'claude-sonnet-4-6', hard: 'claude-opus-4-8' },
|
||||
reviewer: 'claude-fable-5',
|
||||
// @ts-expect-error 测试非法角色被丢弃
|
||||
bogus: 'x',
|
||||
},
|
||||
});
|
||||
assert.deepEqual(up.models, {
|
||||
executor: { easy: 'claude-sonnet-4-6', hard: 'claude-opus-4-8' },
|
||||
reviewer: 'claude-fable-5',
|
||||
});
|
||||
// 读回(走 mapper)一致
|
||||
assert.deepEqual(s.getProject(p.id)!.models, up.models);
|
||||
|
||||
// null 清空
|
||||
const cleared = s.patchProject(p.id, { models: null });
|
||||
assert.equal(cleared.models, null);
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('task scopeFiles:create + patch 持久化与读回,去空白/空数组→null', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'scp', repoPath: '/tmp/scp-' + Math.random() });
|
||||
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy', scopeFiles: [' src/** ', '', 'lib/a.ts'] });
|
||||
assert.deepEqual(t.scopeFiles, ['src/**', 'lib/a.ts']); // 去空白 + 丢空项
|
||||
assert.deepEqual(s.getTask(t.id)!.scopeFiles, ['src/**', 'lib/a.ts']);
|
||||
|
||||
const patched = s.patchTask(t.id, { scopeFiles: ['only.ts'] });
|
||||
assert.deepEqual(patched.scopeFiles, ['only.ts']);
|
||||
|
||||
const emptied = s.patchTask(t.id, { scopeFiles: [] });
|
||||
assert.equal(emptied.scopeFiles, null); // 空数组 → null(不限范围)
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('Easy 路径:写操作 → ready → nextExecutable 领取', () => {
|
||||
const s = freshStore();
|
||||
const p = s.createProject({ name: 'e', repoPath: '/tmp/e-' + Math.random() });
|
||||
|
||||
Reference in New Issue
Block a user