feat(runner): planner prompt扩展+conflict agent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-14 12:20:26 +08:00
parent fa19ff6de1
commit 76d11a8307
+87 -7
View File
@@ -130,14 +130,26 @@ export function buildPlannerPrompt(task: Task, kind: PlanKind): string {
return [
...head, '',
'## 你的角色:任务拆解者(只读,不改任何文件)',
`当前任务层级 depth=${task.depth ?? 1}(根=1),最多拆到 depth=4(剩余可拆层数 ${Math.max(0, 4 - (task.depth ?? 1))} 层)。`,
'只读这个代码库,分析上述(复杂)任务,拆成 2–6 个更小、可独立交付的子任务。不要编辑文件、不要执行有副作用的命令。',
'每个子任务标注复杂度:easy(单文件机械改动/无设计)、medium(需方案、跨几处)、hard(仍需进一步拆解)。',
'## 输出(重要',
'先写一段分析与拆解理由;然后在【最后】输出唯一一个 ```json 代码块,严格格式:',
'每个子任务标注',
'- complexityeasy(单文件机械改动/无设计)、medium(需方案、跨几处)、hard(仍需进一步拆解,仅剩余可拆层数>0时才能用',
'- priority0=P0最紧急 / 1=P1中 / 2=P2最低',
'- deps:依赖本列表中其他子任务的 0-based 序号(无依赖填 []',
'',
'## 输出格式(重要,必须严格遵守)',
'**第一步**:先输出一个 Markdown 表格总览:',
'| # | 子任务标题 | 复杂度 | 优先级 | 依赖序号 |',
'|---|---|---|---|---|',
'(每行一个子任务,依赖序号填 0-based 序号列表如 "0,1" 或留空)',
'',
'**第二步**:再写一段分析与拆解理由。',
'',
'**第三步**:在【最后】输出唯一一个 ```json 代码块,严格格式:',
'```json',
'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy"}]}',
'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy","priority":1,"deps":[]}]}',
'```',
'subtasks 至少 1 个;complexity 只能是 easy|medium|hard;除这个 JSON 块外不要再写其它 ``` 代码块。',
'subtasks 至少 1 个;complexity 只能是 easy|medium|harddeps 是 0-based 序号数组;除这个 JSON 块外不要再写其它 ``` 代码块。',
].join('\n');
}
@@ -153,10 +165,78 @@ export async function runPlanner(task: Task, project: Project, kind: PlanKind, r
cwd: project.repoPath, // 只读跑在主仓(planner 不改文件,无需 worktree
model,
runId,
maxTurns: 60,
maxTurns: task.complexity === 'hard' ? 90 : task.complexity === 'medium' ? 60 : 40,
timeoutMs,
permissionMode: 'acceptEdits', // planner 只读,不会触发编辑;保持工具流不被 prompt 卡住
allowedTools: ['Read', 'Glob', 'Grep'], // 纯只读:无 Edit/Write/Bash,零副作用
allowedTools: ['Read', 'Glob', 'Grep', 'Bash(git log:*)', 'Bash(git diff:*)', 'Bash(git show:*)'], // 纯只读:无 Edit/Write,零副作用
});
return { ok: cc.ok, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: cc.finalText || null, error: cc.error };
}
/** 解冲突任务的提示词:worker 已在 worktree 内触发 git merge 制造冲突态,CC 负责逐个解决。 */
export function buildConflictPrompt(task: Task, conflictFiles: string[]): string {
return [
`# 合并冲突解决:${task.title}`,
`任务 ID${task.id}`,
'',
'## 背景',
'你的工作目录(git worktree)已执行 `git merge` 并产生合并冲突。你的任务是解决全部冲突,保留双方改动的意图,然后提交。',
'',
'## 冲突文件',
conflictFiles.length ? conflictFiles.map((f) => `- ${f}`).join('\n') : '(以 git status 为准)',
'',
'## 执行约束(必须遵守)',
'- 逐个打开冲突文件,理解 <<<<<<< HEAD 与 >>>>>>> 两侧的意图,保留双方有价值的改动,不得丢弃任一方的实质内容。',
'- 解决完所有冲突后,用 `git add -A` 暂存,然后 `git commit`(直接用默认合并 commit message,不要修改)。',
'- 不得执行 git push / git mergemerge 已经发生)/ 切换分支。',
'- 若项目有测试体系且改动可测试,解决冲突后请运行相关测试并在最终回复中说明结果。',
'',
'## 最终回复格式',
'请简要说明:',
'1. 解决了哪些文件的冲突',
'2. 每个文件冲突的取舍策略(保留了哪边的哪些内容)',
'3. 测试运行结果(如有)',
].join('\n');
}
/** 在 worktree 内运行解冲突 agentCC 已看到 git merge 制造的冲突态)。 */
export async function runConflict(
task: Task,
project: Project,
worktree: WorktreeInfo,
runId: string,
conflictFiles: string[],
): Promise<RunnerResult> {
// conflict 角色固定用最强模型(见 models.ts pickModel
const model = pickModel(task, project, 'conflict' as import('./models.js').ModelRole);
const timeoutMs = project.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const cc = await runClaude({
prompt: buildConflictPrompt(task, conflictFiles),
cwd: worktree.dir,
model,
runId,
maxTurns: 80,
timeoutMs,
permissionMode: 'acceptEdits',
allowedTools: [
'Read', 'Edit', 'Write', 'Glob', 'Grep',
'Bash(git status:*)', 'Bash(git diff:*)', 'Bash(git log:*)',
'Bash(git add:*)', 'Bash(git commit:*)',
// 测试命令(同 executor
'Bash(npm test:*)', 'Bash(npm run:*)', 'Bash(npm ci:*)',
'Bash(go build:*)', 'Bash(go test:*)', 'Bash(go vet:*)', 'Bash(go mod:*)',
'Bash(shellcheck:*)', 'Bash(python3 -m pytest:*)', 'Bash(pytest:*)',
],
});
if (!cc.ok) {
return { ok: false, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: null, modelUsed: cc.modelUsed, error: cc.error ?? '解冲突失败' };
}
// 兜底提交(CC 应已 commit,但防万一)
try {
await ensureCommitted(worktree.dir, task);
} catch (e) {
return { ok: false, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: cc.finalText || null, modelUsed: cc.modelUsed, error: `兜底提交失败:${(e as Error).message}` };
}
return { ok: true, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: cc.finalText || null, modelUsed: cc.modelUsed };
}