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:
@@ -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` 章节骨架(如 ## 结论、## 适用性、专业正文各节、## 追溯、## 待确认清单、## 签署 等)的基础上,将专员的草稿进行逻辑润色、出处溯源和终审定稿输出。
|
||||
|
||||
**待确认清单**:在模板对应章节中集中列出文中所有「待确认」。
|
||||
@@ -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] + "..."
|
||||
}
|
||||
@@ -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{"严禁代签", "严禁伪造附件内容"},
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user