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:
+52
-1
@@ -4,7 +4,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { parseChecks, runChecks, diffSizeGate } from '../src/executor/checks.js';
|
||||
import { parseChecks, runChecks, diffSizeGate, globToRegExp, matchesAnyGlob, scopeFileGate } from '../src/executor/checks.js';
|
||||
import type { Project } from '../src/model/types.js';
|
||||
|
||||
function withTmpData(cb: (dir: string) => Promise<void> | void): Promise<void> | void {
|
||||
@@ -94,3 +94,54 @@ test('diffSizeGate:git 出错 → 不拦截(测不出体量)', async () =>
|
||||
const r = await diffSizeGate('/nonexistent-repo-xyz', '/tmp', 'a', 'b');
|
||||
assert.equal(r.ok, true);
|
||||
});
|
||||
|
||||
test('globToRegExp / matchesAnyGlob:* 单层、** 跨层、? 单字符、目录式', () => {
|
||||
assert.ok(globToRegExp('src/*.ts').test('src/a.ts'));
|
||||
assert.ok(!globToRegExp('src/*.ts').test('src/sub/a.ts')); // * 不跨 /
|
||||
assert.ok(globToRegExp('src/**').test('src/sub/deep/a.ts')); // ** 跨 /
|
||||
assert.ok(globToRegExp('src/').test('src/anything/x.ts')); // 目录式 → 其下全部
|
||||
assert.ok(globToRegExp('a?.ts').test('ab.ts'));
|
||||
assert.ok(!globToRegExp('a?.ts').test('abc.ts'));
|
||||
assert.ok(globToRegExp('file.name.js').test('file.name.js')); // . 字面量
|
||||
assert.ok(!globToRegExp('file.name.js').test('fileXname.js'));
|
||||
assert.ok(matchesAnyGlob('lib/x.ts', ['src/**', 'lib/*.ts']));
|
||||
assert.ok(!matchesAnyGlob('test/x.ts', ['src/**', 'lib/*.ts']));
|
||||
});
|
||||
|
||||
test('scopeFileGate:改动越界文件 → 拦截;范围内 → 通过;空声明 → 跳过', async () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), 'maestro-scope-'));
|
||||
const g = (args: string[]) => execFileSync('git', args, { cwd: repo }).toString();
|
||||
g(['init', '-q', '-b', 'main']);
|
||||
g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']);
|
||||
writeFileSync(join(repo, 'base.txt'), 'base\n');
|
||||
g(['add', '-A']); g(['commit', '-qm', 'base']);
|
||||
g(['checkout', '-q', '-b', 'feature']);
|
||||
execFileSync('mkdir', ['-p', join(repo, 'src')]);
|
||||
writeFileSync(join(repo, 'src', 'a.ts'), 'a\n');
|
||||
writeFileSync(join(repo, 'other.txt'), 'o\n'); // 越界文件
|
||||
g(['add', '-A']); g(['commit', '-qm', 'feat']);
|
||||
|
||||
try {
|
||||
// 声明只允许 src/**,但改了 other.txt → 拦截
|
||||
const over = await scopeFileGate(repo, 'feature', 'main', ['src/**']);
|
||||
assert.equal(over.ok, false);
|
||||
assert.deepEqual(over.outside, ['other.txt']);
|
||||
assert.match(over.error!, /声明范围外/);
|
||||
|
||||
// 声明覆盖全部改动 → 通过
|
||||
const ok = await scopeFileGate(repo, 'feature', 'main', ['src/**', 'other.txt']);
|
||||
assert.equal(ok.ok, true);
|
||||
assert.deepEqual(ok.outside, []);
|
||||
|
||||
// 空声明 → 跳过(不限范围)
|
||||
const skip = await scopeFileGate(repo, 'feature', 'main', []);
|
||||
assert.equal(skip.ok, true);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('scopeFileGate:git 出错 → 不拦截', async () => {
|
||||
const r = await scopeFileGate('/nonexistent-repo-xyz', 'a', 'b', ['src/**']);
|
||||
assert.equal(r.ok, true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user