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
+1
View File
@@ -101,6 +101,7 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
if (b.autoApprovePlan !== undefined) patch.autoApprovePlan = Boolean(b.autoApprovePlan);
if (b.autoApproveExec !== undefined) patch.autoApproveExec = Boolean(b.autoApproveExec);
if (b.checks !== undefined) patch.checks = b.checks === null || b.checks === '' ? null : String(b.checks);
if (b.agentRules !== undefined) patch.agentRules = b.agentRules === null || b.agentRules === '' ? null : String(b.agentRules);
return projectOut(store.patchProject(id, patch));
});
+13
View File
@@ -21,6 +21,19 @@ function dataDir(): string {
return process.env.MAESTRO_DATA_DIR ?? join(homedir(), '.maestro');
}
/**
* L4 全局 agent 规范:maestro 自维护的 <data>/agent-global.md(不读用户 ~/.claude/CLAUDE.md
* 避免个人交互习惯与受控执行冲突)。worker 直接读盘注入 prompt(非 DB,不破坏进程模型)。
* 文件不存在/空 → null。
*/
export function readGlobalAgentRules(): string | null {
const p = join(dataDir(), 'agent-global.md');
try {
const txt = readFileSync(p, 'utf8').trim();
return txt || null;
} catch { return null; }
}
/** 全部 run 工作目录根:<data>/runs */
export function runsBase(): string {
return join(dataDir(), 'runs');
+4
View File
@@ -2,6 +2,8 @@ import type { Project, Task, ReviewVerdict, UsageTokens } from '../model/types.j
import type { WorktreeInfo } from './worktree.js';
import { runClaude } from './cc.js';
import { pickModel } from './models.js';
import { rulesHeaderLines } from './runner.js';
import { readGlobalAgentRules } from './protocol.js';
/** 复审角色:codecode review/ security(安全审计)。两者共用 models.ts 的 reviewer 模型映射。 */
export type ReviewRole = 'code' | 'security';
@@ -73,6 +75,8 @@ export function buildReviewPrompt(
`# ${rt.title}${task.title}`,
`任务 ID${task.id}`,
'',
// L4+L2:注入全局/项目规范,让复审据此核对执行者是否守规
...rulesHeaderLines(project, readGlobalAgentRules()),
rt.intro,
'',
'## 任务原始要求',
+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,
+2
View File
@@ -37,6 +37,8 @@ export interface Project {
/** 项目级成本预算(按模型×token 折算累计;超额自动暂停)*/
budgetUsd?: number | null; // 当期预算上限 USDnull=不限)
budgetPeriod?: 'day' | 'month'; // 预算周期(默认 month
/** L2 项目级 agent 执行规范(注入 executor/planner/reviewer/conflict 的 prompt 头部)*/
agentRules?: string | null;
}
export interface ApprovalRecord {
+1
View File
@@ -37,6 +37,7 @@ export function openDb(file: string): Database.Database {
ensureColumn(db, 'projects', 'budget_usd', 'budget_usd REAL'); // 项目当期预算上限 USD(null=不限)
ensureColumn(db, 'projects', 'budget_period', "budget_period TEXT NOT NULL DEFAULT 'month'"); // 预算周期 day|month
ensureColumn(db, 'tasks', 'attachments', 'attachments TEXT'); // 附件 JSON [{name,type,path}](图片/文件随任务提交)
ensureColumn(db, 'projects', 'agent_rules', 'agent_rules TEXT'); // L2 项目级 agent 执行规范(注入 worker prompt
const schema = readFileSync(join(HERE, 'schema.sql'), 'utf8');
db.exec(schema);
return db;
+2
View File
@@ -12,6 +12,7 @@ export interface ProjectRow {
last_sync_at: string | null; logo: string | null; sort_order: number;
checks: string | null; auto_approve_plan: number; auto_approve_exec: number;
budget_usd: number | null; budget_period: string | null;
agent_rules: string | null;
}
export interface TaskRow {
id: string; project_id: string; parent_id: string | null; depth: number;
@@ -51,6 +52,7 @@ export function rowToProject(r: ProjectRow): Project {
autoApproveExec: Boolean(r.auto_approve_exec),
budgetUsd: r.budget_usd ?? null,
budgetPeriod: (r.budget_period === 'day' ? 'day' : 'month'),
agentRules: r.agent_rules ?? null,
};
}
+2 -1
View File
@@ -22,7 +22,8 @@ CREATE TABLE IF NOT EXISTS projects (
auto_approve_plan INTEGER NOT NULL DEFAULT 0, -- 全 easy 子任务时跳过 plan_review0=关)
auto_approve_exec INTEGER NOT NULL DEFAULT 0, -- 双复审 approve 后跳过 exec_review0=关)
budget_usd REAL, -- 项目当期预算上限 USD(null=不限)
budget_period TEXT NOT NULL DEFAULT 'month' -- 预算周期 day | month
budget_period TEXT NOT NULL DEFAULT 'month', -- 预算周期 day | month
agent_rules TEXT -- L2 项目级 agent 执行规范(注入 worker prompt
);
CREATE TABLE IF NOT EXISTS tasks (
+3 -1
View File
@@ -46,6 +46,7 @@ export interface PatchProjectInput {
maxRetries?: number; timeoutMs?: number;
budgetUsd?: number | null; budgetPeriod?: 'day' | 'month';
autoApprovePlan?: boolean; autoApproveExec?: boolean; checks?: string | null;
agentRules?: string | null;
}
export interface PatchTaskInput {
title?: string; priority?: number; complexity?: Complexity; deps?: string[];
@@ -131,7 +132,7 @@ export class Store {
status: 'active', created_at: now(),
last_sync_at: null, logo: null, sort_order: maxOrder + 1,
checks: null, auto_approve_plan: 0, auto_approve_exec: 0,
budget_usd: null, budget_period: 'month',
budget_usd: null, budget_period: 'month', agent_rules: null,
};
this.db.prepare(
`INSERT INTO projects (id,name,repo_path,default_branch,verify_cmd,autonomy,model,concurrency,max_retries,timeout_ms,status,created_at,last_sync_at,logo,sort_order,checks,auto_approve_plan,auto_approve_exec)
@@ -238,6 +239,7 @@ export class Store {
sets.push('auto_approve_exec = @auto_approve_exec'); args.auto_approve_exec = patch.autoApproveExec ? 1 : 0;
}
if (patch.checks !== undefined) { sets.push('checks = @checks'); args.checks = patch.checks; }
if (patch.agentRules !== undefined) { sets.push('agent_rules = @agent_rules'); args.agent_rules = patch.agentRules; }
if (sets.length > 0) {
this.db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = @id`).run(args);
@@ -2,7 +2,11 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildPrompt, buildPlannerPrompt, buildConflictPrompt } from '../src/executor/runner.js';
import { Store } from '../src/store/index.js';
import type { Task } from '../src/model/types.js';
import type { Task, Project } from '../src/model/types.js';
function fakeProject(over: Partial<Project> = {}): Project {
return { defaultBranch: 'main', agentRules: null, ...over } as Project;
}
function fakeTask(over: Partial<Task> = {}): Task {
return {
@@ -43,6 +47,28 @@ test('L1: 超长错误被截断到 2000 字符', () => {
assert.ok(!p.includes('X'.repeat(2100)));
});
test('L2: project.agentRules 注入 executor/planner/conflict/,未设则不注入', () => {
const t = fakeTask();
const withRules = fakeProject({ agentRules: '本项目禁止改 schema.sql' });
assert.ok(buildPrompt(t, withRules).includes('项目规范'));
assert.ok(buildPrompt(t, withRules).includes('禁止改 schema.sql'));
assert.ok(buildPlannerPrompt(fakeTask({ complexity: 'hard' }), 'decompose', withRules).includes('禁止改 schema.sql'));
assert.ok(buildConflictPrompt(t, ['a.ts'], withRules).includes('禁止改 schema.sql'));
// 未设规范 → 不出现「项目规范」标题
assert.ok(!buildPrompt(t, fakeProject()).includes('项目规范'));
assert.ok(!buildPrompt(t).includes('项目规范')); // 连 project 都没传
});
test('L4: globalRules 注入且排在项目规范之前', () => {
const t = fakeTask();
const p = buildPrompt(t, fakeProject({ agentRules: 'PROJ_RULE' }), 'GLOBAL_RULE');
assert.ok(p.includes('全局执行规范'));
assert.ok(p.includes('GLOBAL_RULE'));
assert.ok(p.indexOf('GLOBAL_RULE') < p.indexOf('PROJ_RULE'), '全局规范在项目规范之前');
// 全局规范在任务内容之前(注入顺序:L4→L2→任务内容)
assert.ok(p.indexOf('GLOBAL_RULE') < p.indexOf('任务内容'));
});
test('lastRunErrorOf 取最近一次失败 run 的错误(任意 kind', () => {
const s = new Store(':memory:');
const p = s.createProject({ name: 'l1', repoPath: '/tmp/l1-' + Math.random() });