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:
wangjia
2026-06-25 07:06:32 +08:00
parent 4c09186f36
commit 59ce391ecc
18 changed files with 431 additions and 118 deletions
+16 -5
View File
@@ -77,6 +77,15 @@ function lastErrorLines(task: Task): string[] {
];
}
/** 声明文件范围约束(task.scopeFiles 非空时注入):改动越界文件会被硬闸拦截、任务打回。 */
function scopeConstraintLines(task: Task): string[] {
const scope = task.scopeFiles?.filter((s) => s.trim());
if (!scope || scope.length === 0) return [];
return [
`- 本任务声明的改动文件范围(仅允许改动这些 glob/路径,越界会被硬闸拦截并打回):${scope.map((s) => `\`${s}\``).join('、')}`,
];
}
/** 组装任务提示词:标题 + 全局/项目规范(L4/L2) + operations/spec 全文 + 上次失败(L1) + 执行约束 */
export function buildPrompt(task: Task, project?: Project, globalRules?: string | null): string {
const body = task.operations ?? task.spec ?? task.plan ?? '';
@@ -92,6 +101,7 @@ export function buildPrompt(task: Task, project?: Project, globalRules?: string
...lastErrorLines(task),
'## 执行约束(必须遵守)',
'- 只在当前工作目录(git worktree)内改动文件,不得读写或修改 worktree 之外的任何文件。',
...scopeConstraintLines(task),
`- 完成后用 \`git add -A && git commit\` 提交全部改动,commit message 必须包含任务 ID「${task.id}」。`,
'- 不得执行 git push / git merge / 切换分支,当前分支就是你的工作分支。',
'- 不要发布、部署或执行任何有外部副作用的操作。',
@@ -198,20 +208,21 @@ export function buildPlannerPrompt(task: Task, kind: PlanKind, project?: Project
'- complexityeasy(单文件机械改动/无设计)、medium(需方案、跨几处)、hard(仍需进一步拆解,仅剩余可拆层数>0时才能用)',
'- priority0=P0最紧急 / 1=P1中 / 2=P2最低',
'- deps:依赖本列表中其他子任务的 0-based 序号(无依赖填 []',
'- files:该子任务预计改动的文件范围(glob/路径,如 ["src/foo/**","lib/a.ts"])。尽量精确——执行时改动越界文件会被硬闸拦截。无法确定就留空数组(不限范围)。',
'',
'## 输出格式(重要,必须严格遵守)',
'**第一步**:先输出一个 Markdown 表格总览:',
'| # | 子任务标题 | 复杂度 | 优先级 | 依赖序号 |',
'|---|---|---|---|---|',
'(每行一个子任务,依赖序号填 0-based 序号列表如 "0,1" 或留空)',
'| # | 子任务标题 | 复杂度 | 优先级 | 依赖序号 | 文件范围 |',
'|---|---|---|---|---|---|',
'(每行一个子任务,依赖序号填 0-based 序号列表如 "0,1" 或留空;文件范围填预计改动的路径/glob 或留空',
'',
'**第二步**:再写一段分析与拆解理由。',
'',
'**第三步**:在【最后】输出唯一一个 ```json 代码块,严格格式:',
'```json',
'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy","priority":1,"deps":[]}]}',
'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy","priority":1,"deps":[],"files":["src/foo/**"]}]}',
'```',
'subtasks 至少 1 个;complexity 只能是 easy|medium|harddeps 是 0-based 序号数组;除这个 JSON 块外不要再写其它 ``` 代码块。',
'subtasks 至少 1 个;complexity 只能是 easy|medium|harddeps 是 0-based 序号数组;files 是文件 glob/路径数组(可空);除这个 JSON 块外不要再写其它 ``` 代码块。',
].join('\n');
}