Files
agent-bot 2c75dcaf69
CI / go-vet (pull_request) Successful in 13s
CI / short-tests-report (pull_request) Failing after 35s
CI / backup-restore-drill (pull_request) Successful in 36s
CI / web (pull_request) Successful in 32s
refactor(squad): AssetProvider port replaces direct template dependency
KTD1 fifth step (behavior preserved):
- squad.AssetProvider{DocTemplate,RenderDoc,AgentInstruction,
  AgentInstructionFile} injected via Options.Assets into Service,
  WorkspaceEngine and AdaptivePlanner
- bizdev templateAssetProvider maps port to internal/template (assembly side)
- nil-provider paths degrade to the same fallbacks the old error branches
  used (placeholder template, role header) — fail-visible, no guessing
- squad -> template import eliminated; arch edge moved bizdev->template
- verified: build/vet/arch green, squad+bizdev short tests pass
2026-09-29 06:29:51 +00:00

161 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package squad
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// buildSubWorkspaces 为 workflow 中的各个子任务构建独立沙箱
func (e *WorkspaceEngine) buildSubWorkspaces(workDir string, wf WorkflowPlan, squadCfg SquadConfig, renderedTmpl string) error {
agentMap := make(map[string]AgentSpec)
for _, a := range squadCfg.Agents {
agentMap[a.Role] = a
}
for _, step := range wf.Steps {
subDir := filepath.Join(workDir, filepath.FromSlash(step.SubWorkspace))
if err := os.MkdirAll(subDir, 0o755); err != nil {
return err
}
// 1. 生成 AGENTS.md:优先 AgentSpec.InstructionsFile,再按角色名回落,再 inline 渲染
var agentsBuilder strings.Builder
spec := agentMap[step.Role]
var inst string
var err error
if e.assets != nil {
if file := strings.TrimSpace(spec.InstructionsFile); file != "" {
inst, err = e.assets.AgentInstructionFile(file)
}
if err != nil || strings.TrimSpace(inst) == "" {
inst, err = e.assets.AgentInstruction(step.Role)
}
}
if err == nil && strings.TrimSpace(inst) != "" {
agentsBuilder.WriteString(inst)
} else {
agentsBuilder.WriteString(fmt.Sprintf("# 角色: %s (%s)\n\n", spec.Name, spec.Role))
if len(spec.Responsibilities) > 0 {
agentsBuilder.WriteString("## 核心职责\n")
for _, r := range spec.Responsibilities {
agentsBuilder.WriteString(fmt.Sprintf("- %s\n", r))
}
agentsBuilder.WriteString("\n")
}
if len(spec.ForbiddenActions) > 0 {
agentsBuilder.WriteString("## 严禁行为(红线)\n")
for _, f := range spec.ForbiddenActions {
agentsBuilder.WriteString(fmt.Sprintf("- ❌ %s\n", f))
}
agentsBuilder.WriteString("\n")
}
}
_ = os.WriteFile(filepath.Join(subDir, "AGENTS.md"), []byte(agentsBuilder.String()), 0o644)
_ = os.WriteFile(filepath.Join(subDir, "INSTRUCTIONS.md"), []byte(agentsBuilder.String()), 0o644)
// 2. 生成 TASK.md
deliverableName := "output.md"
if step.Mode == "plan" || step.Mode == "replan" {
deliverableName = "output_plan.md"
} else if step.Mode == "review_plan" {
deliverableName = "review_notes.md"
}
var taskBuilder strings.Builder
if step.InputLayout == InputLayoutSnapshot {
if err := SnapshotInputsIntoSubdir(workDir, subDir, step.Needs, deliverableName); err != nil {
return err
}
taskText := BuildContainedTaskMarkdown(step, deliverableName)
if TaskMarkdownEscapesParent(taskText) {
return fmt.Errorf("snapshot TASK.md must not reference parent paths")
}
if err := writeRegularFile(filepath.Join(subDir, "TASK.md"), []byte(taskText)); err != nil {
return err
}
inputRefs := map[string]any{
"step_id": step.ID,
"role": step.Role,
"needs": step.Needs,
"target_file": deliverableName,
"input_layout": InputLayoutSnapshot,
"forbid_parent_paths": true,
}
_ = writeJSON(filepath.Join(subDir, "input_refs.json"), inputRefs)
continue
}
taskBuilder.WriteString(fmt.Sprintf("# 任务执行指令: %s\n\n", step.Title))
taskBuilder.WriteString(fmt.Sprintf("- **步骤 ID**: `%s`\n", step.ID))
taskBuilder.WriteString(fmt.Sprintf("- **分配角色**: `%s`\n", step.Role))
taskBuilder.WriteString(fmt.Sprintf("- **执行模式**: `%s`\n", step.Mode))
taskBuilder.WriteString(fmt.Sprintf("- **交付产物**: `%s`\n\n", deliverableName))
taskBuilder.WriteString("## 环境与输入导航\n")
taskBuilder.WriteString("请仔细查阅上级目录资源:\n")
taskBuilder.WriteString("- `../../context/index.json` 及 `../../context/README.md`:阅读上下文指引\n")
taskBuilder.WriteString("- `../../template.md`:自描述交付模板(嵌入四段式与格式约束)\n")
taskBuilder.WriteString("- `../../blackboard.json`:前序专员抽取的结构化事实与缺口\n")
taskBuilder.WriteString("- `../../context/project.md`:项目全局立项基线与产品定义\n")
taskBuilder.WriteString("- `../../context/user.md`:用户个性化工作偏好与决策约束\n")
taskBuilder.WriteString("- `../../context/dialogue/`:项目成员共享对话(纪要/近窗/事实/冲突裁决)\n")
taskBuilder.WriteString("- `../../context/materials_text/README.md`:原始材料全文或基线说明\n")
taskBuilder.WriteString("- `../../context/knowledge/hits.md`:法规与标准命中快照 (若存在)\n\n")
if len(step.Needs) > 0 {
taskBuilder.WriteString("## 前序依赖产出\n")
for _, n := range step.Needs {
taskBuilder.WriteString(fmt.Sprintf("- `../../%s`\n", filepath.ToSlash(n)))
}
taskBuilder.WriteString("\n")
}
if len(step.Accept) > 0 {
taskBuilder.WriteString("## 验收标准(Checklist)\n")
for _, a := range step.Accept {
taskBuilder.WriteString(fmt.Sprintf("- [ ] %s\n", a))
}
taskBuilder.WriteString("\n")
}
if strings.HasPrefix(wf.TaskKey, "rd_") {
overlay := RDevStageOverlay(wf.TaskKey, step.Role, rdevTierFromWorkspace(workDir))
if overlay != "" {
_ = os.WriteFile(filepath.Join(subDir, "OVERLAY.md"), []byte(overlay+"\n"), 0o644)
taskBuilder.WriteString("## 阶段纪律(overlay)\n")
taskBuilder.WriteString("写作纪律与粒度下限见同目录 `OVERLAY.md`。模板只规定章节结构,不得用 overlay 改写章节骨架。\n\n")
}
}
taskBuilder.WriteString("## 交付契约与写作纪律\n")
taskBuilder.WriteString(fmt.Sprintf("请在当前目录下生成完整交付物 `%s`:\n", deliverableName))
taskBuilder.WriteString("1. 严格四段式展开(结论句 → 表格/列表 → 解读 → 支撑出处)。\n")
taskBuilder.WriteString("2. 各章节必须实质展开;禁止以篇幅借口或省略写法跳过应写细节(自检时勿复述禁用原词)。\n")
taskBuilder.WriteString("3. 关键事实必须标注出处,未知项列入待确认清单。\n")
if step.Role == "organizer" {
taskBuilder.WriteString("4. 从材料提取的客观字段,请在当前工作目录下输出 `fields.json`。\n")
}
_ = os.WriteFile(filepath.Join(subDir, "TASK.md"), []byte(taskBuilder.String()), 0o644)
// 3. 写入 input_refs.json
inputRefs := map[string]any{
"step_id": step.ID,
"role": step.Role,
"needs": step.Needs,
"target_file": deliverableName,
}
_ = writeJSON(filepath.Join(subDir, "input_refs.json"), inputRefs)
// 4. 创建便捷软链接,使得当前目录下无论是读 template.md、context/ 还是 ../../ 均可访问
_ = os.Symlink(filepath.Join("..", "..", "template.md"), filepath.Join(subDir, "template.md"))
_ = os.Symlink(filepath.Join("..", "..", "context"), filepath.Join(subDir, "context"))
_ = os.Symlink(filepath.Join("..", "..", "blackboard.json"), filepath.Join(subDir, "blackboard.json"))
}
return nil
}