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
161 lines
6.7 KiB
Go
161 lines
6.7 KiB
Go
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
|
||
}
|