feat(agent): L2/L4 记忆注入——项目规范 + 全局规范文件喂进 4 处 prompt

- L2 projects.agent_rules 列(schema+迁移+类型+mapper+patchProject+API 透传)
- L4 protocol.readGlobalAgentRules():worker 直接读 <data>/agent-global.md(非DB,
  不读用户 ~/.claude/CLAUDE.md,避免个人习惯与受控执行冲突)
- runner.rulesHeaderLines 注入 executor/planner/conflict;reviewer 同注入
- 顺序:全局规范(L4)→项目规范(L2)→任务内容→上次失败(L1)→执行约束

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 22:29:29 +08:00
parent ff2d7c22e7
commit fe7923a618
10 changed files with 81 additions and 13 deletions
+26 -10
View File
@@ -2,6 +2,7 @@ import type { Project, Task, UsageTokens } from '../model/types.js';
import { git, type WorktreeInfo } from './worktree.js';
import { runClaude } from './cc.js';
import { pickModel } from './models.js';
import { readGlobalAgentRules } from './protocol.js';
export { transcriptDir } from './cc.js';
@@ -25,6 +26,19 @@ const DEFAULT_MAX_TURNS = 100;
/** 若项目未配置 timeoutMs 时的全局兜底超时(30min */
const DEFAULT_TIMEOUT_MS = 30 * 60_000;
/**
* L4+L2 记忆注入:prompt 头部规范段。全局规范(L4maestro 自维护 agent-global.md)在前,
* 项目规范(L2project.agentRules)在后。两者皆空 → 空数组。
*/
export function rulesHeaderLines(project?: Project, globalRules?: string | null): string[] {
const out: string[] = [];
const g = globalRules?.trim();
if (g) out.push('## 全局执行规范(所有任务通用,必须遵守)', g, '');
const pr = project?.agentRules?.trim();
if (pr) out.push('## 项目规范(本项目专属,必须遵守)', pr, '');
return out;
}
/**
* L1 记忆注入:若任务带「上次失败原因」(重试任务),生成一段提醒;否则空数组。
* 由 daemon 在 claimOne 时把 store.lastRunErrorOf 填进 task.lastRunError 下发(见 orchestrator)。
@@ -42,13 +56,14 @@ function lastErrorLines(task: Task): string[] {
];
}
/** 组装任务提示词:标题 + operations/spec 全文 + 上次失败(L1) + 执行约束 */
export function buildPrompt(task: Task): string {
/** 组装任务提示词:标题 + 全局/项目规范(L4/L2) + operations/spec 全文 + 上次失败(L1) + 执行约束 */
export function buildPrompt(task: Task, project?: Project, globalRules?: string | null): string {
const body = task.operations ?? task.spec ?? task.plan ?? '';
return [
`# 任务:${task.title}`,
`任务 ID${task.id}`,
'',
...rulesHeaderLines(project, globalRules),
'## 任务内容',
body || '(无详细说明,按标题完成)',
'',
@@ -81,7 +96,7 @@ export async function runTask(task: Task, project: Project, worktree: WorktreeIn
const model = pickModel(task, project, 'executor');
const timeoutMs = project.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const cc = await runClaude({
prompt: buildPrompt(task),
prompt: buildPrompt(task, project, readGlobalAgentRules()),
cwd: worktree.dir,
model,
runId,
@@ -133,13 +148,13 @@ export interface PlannerResult {
export type PlannerFn = (task: Task, project: Project, kind: PlanKind, runId: string) => Promise<PlannerResult>;
/** planner 提示词:只读代码库,spec=写改动方案;decompose=拆子任务并在末尾输出 fenced JSON。 */
export function buildPlannerPrompt(task: Task, kind: PlanKind): string {
const head = [`# 任务:${task.title}`, `任务 ID${task.id}`];
export function buildPlannerPrompt(task: Task, kind: PlanKind, project?: Project, globalRules?: string | null): string {
const head = [`# 任务:${task.title}`, `任务 ID${task.id}`, '', ...rulesHeaderLines(project, globalRules)];
const errLines = lastErrorLines(task);
if (kind === 'spec') {
if (task.spec) head.push('', '## 现有方案草稿(可改进/替换)', task.spec);
if (task.spec) head.push('## 现有方案草稿(可改进/替换)', task.spec, '');
return [
...head, '',
...head,
...errLines,
'## 你的角色:方案作者(只读,不改任何文件)',
'只读这个代码库,为上述任务写一份「具体改动方案」。不要编辑文件、不要执行有副作用的命令。',
@@ -186,7 +201,7 @@ export async function runPlanner(task: Task, project: Project, kind: PlanKind, r
const model = pickModel(task, project, 'planner');
const timeoutMs = project.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const cc = await runClaude({
prompt: buildPlannerPrompt(task, kind),
prompt: buildPlannerPrompt(task, kind, project, readGlobalAgentRules()),
cwd: project.repoPath, // 只读跑在主仓(planner 不改文件,无需 worktree
model,
runId,
@@ -199,11 +214,12 @@ export async function runPlanner(task: Task, project: Project, kind: PlanKind, r
}
/** 解冲突任务的提示词:worker 已在 worktree 内触发 git merge 制造冲突态,CC 负责逐个解决。 */
export function buildConflictPrompt(task: Task, conflictFiles: string[]): string {
export function buildConflictPrompt(task: Task, conflictFiles: string[], project?: Project, globalRules?: string | null): string {
return [
`# 合并冲突解决:${task.title}`,
`任务 ID${task.id}`,
'',
...rulesHeaderLines(project, globalRules),
'## 背景',
'你的工作目录(git worktree)已执行 `git merge` 并产生合并冲突。你的任务是解决全部冲突,保留双方改动的意图,然后提交。',
'',
@@ -237,7 +253,7 @@ export async function runConflict(
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),
prompt: buildConflictPrompt(task, conflictFiles, project, readGlobalAgentRules()),
cwd: worktree.dir,
model,
runId,