From 76d11a83073de102546968c3e016c2241ced0703 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 14 Jun 2026 12:20:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(runner):=20planner=20prompt=E6=89=A9?= =?UTF-8?q?=E5=B1=95+conflict=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/executor/runner.ts | 94 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/src/executor/runner.ts b/src/executor/runner.ts index dc0db9c..8f50bcc 100644 --- a/src/executor/runner.ts +++ b/src/executor/runner.ts @@ -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 代码块,严格格式:', + '每个子任务标注:', + '- complexity:easy(单文件机械改动/无设计)、medium(需方案、跨几处)、hard(仍需进一步拆解,仅剩余可拆层数>0时才能用)', + '- priority:0=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|hard;deps 是 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 merge(merge 已经发生)/ 切换分支。', + '- 若项目有测试体系且改动可测试,解决冲突后请运行相关测试并在最终回复中说明结果。', + '', + '## 最终回复格式', + '请简要说明:', + '1. 解决了哪些文件的冲突', + '2. 每个文件冲突的取舍策略(保留了哪边的哪些内容)', + '3. 测试运行结果(如有)', + ].join('\n'); +} + +/** 在 worktree 内运行解冲突 agent(CC 已看到 git merge 制造的冲突态)。 */ +export async function runConflict( + task: Task, + project: Project, + worktree: WorktreeInfo, + runId: string, + conflictFiles: string[], +): Promise { + // 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 }; +}