feat(runner): planner prompt扩展+conflict agent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+87
-7
@@ -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<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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user