fix(squad): inject rdev_leader InstructionsFile for DHF final review

Prefer AgentSpec.InstructionsFile when building AGENTS.md so rd_* leader
steps no longer fall back to business-tone leader.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 09:46:37 +00:00
co-authored by Cursor
parent aba38ef7a9
commit dc3aef4607
5 changed files with 133 additions and 6 deletions
+30
View File
@@ -0,0 +1,30 @@
## 角色
你是研发/设计控制小队的负责人(花名册名称:研发设计控制负责人)。按 ISO 13485 把本阶段做成可追溯的 DHF 记录。你是用户与小队之间的唯一接口:专员向你汇报,只有你对用户说话、提交阶段文档。
你的价值不在于自己写长文,而在于把本阶段设计控制要求变成可验收的任务,审核专员产出,整合成一份可提交的 Markdown 终稿。结论短、论证完整:卡片可以短,DHF 文档不能短。
**明确边界**:不做报价核算与商务承诺,不写商务对接口径。
## 汇报纪律
- 只有你可以把成果交给用户(通过系统落文档)。不要把未审核的专员原文直接当终稿。
- 需要用户决策的事项一律上升,不要代用户拍板。
- 专员产出不合格时退回说明差距;系统按剧本执行,你负责审核与整合。
## 本阶段你要做的(DHF 终审)
1. 用一句话回述本阶段交付物及其 DHF 归属。
2. 阅读快照、材料文本、前序文档和各专员 `output.md`。
3. 按本阶段 RD 模板整合终稿,结论先行;与前序已批准文件保持一致。
4. **追溯表必填核查**:本文件 ID、上游输入、下游验证/确认;未知项写入「待确认」而非留空。
5. **不适用理由完整性**:声明不适用时须写完整理由,禁止空壳「不适用」。
6. 缺信息标「待确认」,**禁止编造**试验数据、标准号、URL、签字、附件正文。
7. 按 TASK.md 指定的交付文件落盘:终稿步写 `output.md`;若本步是「审核撰写计划」(review_plan),只写 `review_notes.md`,**禁止**把审核意见写入或覆盖 `output.md`。
8. 专员已标注的出处必须保留;你整合时把能溯源的句子标上〔…〕,无依据的分析不要删,也不要补造引用。
## 汇报格式(终稿 output.md 开头;计划审核则写在 review_notes.md)
**注意**:你整合生成的终稿文档结构必须严格遵循 `template.md` 的所有章节结构,不得自行替换为审阅报告结构!你的审阅和把关成果体现为:在保持 `template.md` 章节骨架(如 ## 结论、## 适用性、专业正文各节、## 追溯、## 待确认清单、## 签署 等)的基础上,将专员的草稿进行逻辑润色、出处溯源和终审定稿输出。
**待确认清单**:在模板对应章节中集中列出文中所有「待确认」。
+73
View File
@@ -0,0 +1,73 @@
package squad_test
import (
"os"
"path/filepath"
"strings"
"testing"
"chat-agent-system/internal/squad"
)
func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) {
tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine()
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "rd_design_input_spec"),
TaskKey: "rd_design_input_spec",
StageTitle: "设计输入规格书",
ProjectName: "rdev-s0-prompt",
Backend: "stub",
})
if err != nil {
t.Fatalf("PrepareWorkspace rd_design_input_spec: %v", err)
}
leaderAgents := filepath.Join(ws.WorkDir, "tasks", "04_review", "AGENTS.md")
body, err := os.ReadFile(leaderAgents)
if err != nil {
t.Fatalf("read leader AGENTS.md: %v", err)
}
text := string(body)
if !strings.Contains(text, "设计控制") {
t.Fatalf("rdev leader AGENTS.md must mention 设计控制; got snippet:\n%s", truncate(text, 400))
}
for _, banned := range []string{"商务对接负责人", "CDO"} {
if strings.Contains(text, banned) {
t.Fatalf("rdev leader AGENTS.md must not contain %q", banned)
}
}
}
func TestIntakeLeaderPrompt_StillBusiness(t *testing.T) {
tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine()
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "intake"),
TaskKey: "intake",
StageTitle: "用户初始需求收集表",
ProjectName: "intake-prompt-regression",
Backend: "stub",
})
if err != nil {
t.Fatalf("PrepareWorkspace intake: %v", err)
}
leaderAgents := filepath.Join(ws.WorkDir, "tasks", "03_review", "AGENTS.md")
body, err := os.ReadFile(leaderAgents)
if err != nil {
t.Fatalf("read intake leader AGENTS.md: %v", err)
}
if !strings.Contains(string(body), "商务对接负责人") {
t.Fatalf("intake leader AGENTS.md must retain 商务对接负责人")
}
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
+1
View File
@@ -91,6 +91,7 @@ func rdevLeaderAgent() AgentSpec {
Model: "claude-3-7-sonnet",
Tools: []string{"read", "write", "edit"},
BashPermission: "deny",
InstructionsFile: "configs/agents/rdev_leader.md",
Responsibilities: []string{"DHF 完整性终审", "不适用理由审核", "禁止编造检查"},
ForbiddenActions: []string{"严禁代签", "严禁伪造附件内容"},
}
+11 -4
View File
@@ -468,13 +468,20 @@ func (e *WorkspaceEngine) buildSubWorkspaces(workDir string, wf WorkflowPlan, sq
return err
}
// 1. 生成 AGENTS.md
// 1. 生成 AGENTS.md:优先 AgentSpec.InstructionsFile,再按角色名回落,再 inline 渲染
var agentsBuilder strings.Builder
inst, err := template.GetAgentInstruction(step.Role)
if err == nil {
spec := agentMap[step.Role]
var inst string
var err error
if file := strings.TrimSpace(spec.InstructionsFile); file != "" {
inst, err = template.GetAgentInstructionFile(file)
}
if err != nil || strings.TrimSpace(inst) == "" {
inst, err = template.GetAgentInstruction(step.Role)
}
if err == nil && strings.TrimSpace(inst) != "" {
agentsBuilder.WriteString(inst)
} else {
spec := agentMap[step.Role]
agentsBuilder.WriteString(fmt.Sprintf("# 角色: %s (%s)\n\n", spec.Name, spec.Role))
if len(spec.Responsibilities) > 0 {
agentsBuilder.WriteString("## 核心职责\n")
+18 -2
View File
@@ -56,11 +56,27 @@ func GetDocTemplate(name string) (string, error) {
func GetAgentInstruction(role string) (string, error) {
role = strings.TrimSuffix(role, ".md")
if role == "" || strings.Contains(role, "..") || strings.ContainsAny(role, `/\`) {
return "", fmt.Errorf("read agent instruction: invalid role %q", role)
}
return GetAgentInstructionFile(filepath.Join("configs", "agents", role+".md"))
}
// GetAgentInstructionFile 读取 configs/agents/ 下指定相对路径的指令文件,
// 并自动追加 configs/shared/writing.md。rel 必须落在 configs/agents/ 内,拒绝 ".."。
func GetAgentInstructionFile(rel string) (string, error) {
rel = filepath.ToSlash(strings.TrimSpace(rel))
if rel == "" || strings.Contains(rel, "..") {
return "", fmt.Errorf("read agent instruction file: invalid path %q", rel)
}
if !strings.HasPrefix(rel, "configs/agents/") {
return "", fmt.Errorf("read agent instruction file: must be under configs/agents/: %q", rel)
}
root := findProjectRoot()
path := filepath.Join(root, "configs", "agents", role+".md")
path := filepath.Join(root, filepath.FromSlash(rel))
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read agent instruction %s: %w", role, err)
return "", fmt.Errorf("read agent instruction file %s: %w", rel, err)
}
inst := string(b)