package squad import ( "context" "fmt" "os" "path/filepath" "strings" "sync" "time" "agent-native/exec" ) // flowDriver 工作流驱动器:推进 runState 直到终态或 HITL 挂起边界。 type flowDriver struct { opts engineDeps } // advance 执行到边界;返回 (结果, 挂起上下文),二者互斥非 nil(infra 错误时都为 nil 且 err 非 nil)。 func (d *flowDriver) advance(ctx context.Context, st *runState) (res *RunResult, gate *hitlGate, err error) { wf := st.Workflow() wf.Status = "in_progress" st.setWorkflow(wf) d.emit(st, Event{Type: EvPhase, Text: "开始执行工作流"}) i := 0 for i < len(st.Plan.Workflow.Steps) { step := st.Plan.Workflow.Steps[i] switch step.Status { case "completed", "skipped", "failed": i++ continue } // HITL:角色权限升级(bash=ask 且本运行未授权过该角色)。 role, _ := findAgent(st.Plan.Squad.Agents, step.Role) if role.BashPermission == "ask" && !st.Approved["role:"+role.Role] { switch d.policy(st).mode() { case HitlDisabled, HitlAuto: step.Error = "permission denied: role " + role.Role + " requires bash approval (hitl mode=" + string(d.policy(st).mode()) + ")" st.Plan.Workflow.Steps[i] = step return d.failStep(ctx, st, i) default: return d.hold(st, HitlPermission, step.ID, step.Role, fmt.Sprintf("角色 %s(%s)声明 bash_permission=ask,是否授权其在本次运行中使用更高权限?", role.Name, role.Role), []HumanOption{ {Value: "approve", Label: "授权本次运行"}, {Value: "reject", Label: "拒绝,步骤失败"}, {Value: "abort", Label: "中止运行"}, }, fmt.Sprintf("步骤:%s\n角色:%s\n禁忌:%v", step.Title, role.Role, role.ForbiddenActions), nil) } } // HITL:步骤显式要求审批。 if step.RequiresApproval && !st.Approved["step:"+step.ID] { switch d.policy(st).mode() { case HitlDisabled, HitlAuto: step.Error = "approval required but hitl mode is " + string(d.policy(st).mode()) st.Plan.Workflow.Steps[i] = step return d.failStep(ctx, st, i) default: return d.hold(st, HitlConfirmation, step.ID, step.Role, fmt.Sprintf("步骤「%s」标记为需人工审批后才能执行,是否批准?", step.Title), []HumanOption{ {Value: "approve", Label: "批准执行"}, {Value: "skip", Label: "跳过该步骤"}, {Value: "abort", Label: "中止运行"}, }, fmt.Sprintf("角色:%s\n产出:%v", step.Role, step.Targets), nil) } } if step.Parallel { // 收集相邻并行组,组内并发、组间顺序。 var group []int for i < len(st.Plan.Workflow.Steps) { s := st.Plan.Workflow.Steps[i] if s.Parallel && s.Status != "completed" && s.Status != "skipped" && s.Status != "failed" { group = append(group, i) i++ continue } break } if out, g, err := d.runParallel(ctx, st, group); out != nil || g != nil || err != nil { if out == nil && g != nil { out = heldResult(st, g) } return out, g, err } if g2 := d.checkBlockingGaps(st); g2 != nil { return heldResult(st, g2), g2, nil } continue } out, g, err := d.runOneStep(ctx, st, i) if out != nil || g != nil || err != nil { if out == nil && g != nil { out = heldResult(st, g) } return out, g, err } i++ // 阻塞缺口挂起。 if g2 := d.checkBlockingGaps(st); g2 != nil { return heldResult(st, g2), g2, nil } // 动态调整(仅顺序步骤后,避免并发竞态)。 if st.Plan.Workflow.Steps[i-1].AllowDynamicAdjustment && d.opts.Evaluator != nil { out, g, err := d.applyDynamicAdjustment(ctx, st, i-1) if out != nil || g != nil || err != nil { if out == nil && g != nil { out = heldResult(st, g) } return out, g, err } } } return d.finalize(ctx, st) } // runParallel 并发执行一个并行组;关键步骤失败即终止整个运行。 func (d *flowDriver) runParallel(ctx context.Context, st *runState, idxs []int) (*RunResult, *hitlGate, error) { var wg sync.WaitGroup var mu sync.Mutex failedCritical := -1 for _, idx := range idxs { wg.Add(1) go func(idx int) { defer wg.Done() out, _, err := d.runOneStep(ctx, st, idx) mu.Lock() defer mu.Unlock() if (out != nil && out.Status == RunFailed) || err != nil { if st.Plan.Workflow.Steps[idx].IsCritical() && failedCritical < 0 { failedCritical = idx } } }(idx) } wg.Wait() if failedCritical >= 0 { return d.failStep(ctx, st, failedCritical) } return nil, nil, nil } // runOneStep 执行单个步骤(含瞬态重试与交付物校验)。 func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) { step := st.Plan.Workflow.Steps[idx] role, ok := findAgent(st.Plan.Squad.Agents, step.Role) if !ok { if r, exists := d.opts.Registry.Get(step.Role); exists { role = r st.mu.Lock() st.Plan.Squad.Agents = append(st.Plan.Squad.Agents, r) st.mu.Unlock() } else { step.Status, step.Error = "failed", "role not found: "+step.Role st.setStep(idx, step) return d.failStep(ctx, st, idx) } } subDir := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(orDefault(step.SubWorkspace, fmt.Sprintf("tasks/%02d_%s", idx+1, step.Role)))) if err := os.MkdirAll(subDir, 0o755); err != nil { return nil, nil, err } if step.SubWorkspace == "" { step.SubWorkspace = filepath.ToSlash(filepath.Join("tasks", fmt.Sprintf("%02d_%s", idx+1, step.Role))) } d.emit(st, Event{Type: EvStepStarted, StepID: step.ID, Role: step.Role, Text: step.Title}) step.Status = "running" step.Error = "" st.setStep(idx, step) started := time.Now() prompt := readFileOr(filepath.Join(subDir, "TASK.md"), step.Title) prompt = st.injectSteering(prompt) instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), d.opts.Workspaces.ResolveInstructions(role)) backend, backendName, err := d.opts.Backends.Resolve(role, st.Meta.Backend) if err != nil { step.Status, step.Error = "failed", err.Error() st.setStep(idx, step) return d.failStep(ctx, st, idx) } targets := step.Targets if len(targets) == 0 { targets = []string{"output.md"} } var result *exec.Result for attempt := 1; attempt <= exec.MaxTransientStepInvocations; attempt++ { reqPrompt := prompt if attempt > 1 { prevReason := "交付物缺失" if err != nil { prevReason = err.Error() } else if result != nil && result.ErrorReason != "" { prevReason = result.ErrorReason } reqPrompt += "\n\n【重试提示】上一次执行未成功(" + prevReason + ")。请务必把结果写入:" + strings.Join(targets, ", ") } req := exec.Request{ WorkDir: subDir, Prompt: reqPrompt, Instructions: instructions, Targets: targets, Timeout: d.opts.StepTimeout, ToolProfile: orDefault(role.ToolProfile, exec.ToolProfileWorkspace), OnEvent: func(ev exec.ProcessEvent) { d.emit(st, Event{Type: EvDetail, StepID: step.ID, Role: step.Role, Text: ev.Text, Data: marshalJSON(map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail})}) }, } result, err = backend.Execute(ctx, req) if err == nil && result.Success { break } // 交付物物化:后端返回 patches 但未写盘时补写。 if err == nil && len(result.Patches) > 0 { if _, merr := exec.MaterializeTargets(subDir, targets, result.Patches); merr == nil { if allTargetsPresent(subDir, targets) { result.Success = true break } } } assessment := exec.AssessStepFailure(exec.StepFailureSignals{ Err: err, Result: result, WorkDir: subDir, ExtraText: fmt.Sprintf("step=%s backend=%s", step.ID, backendName), TargetMissing: !allTargetsPresent(subDir, targets), }) if assessment.Class != exec.FailureTransient || attempt == exec.MaxTransientStepInvocations { step.Status = "failed" if err != nil { step.Error = err.Error() } else { step.Error = orDefault(assessment.Reason, result.ErrorReason) } break } } if ctx.Err() != nil { step.Status = "pending" // 取消后可恢复 st.setStep(idx, step) return &RunResult{RunID: st.Meta.ID, Status: RunCancelled, Error: "context cancelled", Plan: planPtr(st), WorkDir: st.Meta.WorkDir}, nil, nil } if result != nil { st.addUsage(step.Role, result.PromptTokens, result.CompletionTokens) } step.DurationMs = time.Since(started).Milliseconds() if step.Status == "failed" { d.emit(st, Event{Type: EvStepFailed, StepID: step.ID, Role: step.Role, Text: step.Error}) st.setStep(idx, step) return d.failStep(ctx, st, idx) } // 交付物校验。 missing := missingTargets(subDir, targets) if len(missing) > 0 { step.Status, step.Error = "failed", fmt.Sprintf("missing deliverables: %v", missing) d.emit(st, Event{Type: EvStepFailed, StepID: step.ID, Role: step.Role, Text: step.Error}) st.setStep(idx, step) return d.failStep(ctx, st, idx) } step.Status = "completed" appendLog(subDir, fmt.Sprintf("[%s] backend=%s duration=%dms targets=%v\n", time.Now().Format(time.RFC3339), backendName, step.DurationMs, targets)) // 状态回填 + 黑板合并 + 落盘同一临界区:并行步骤的共享状态变更串行化, // 防止 marshal 与写回竞争、临时文件互相踩踏把保存失败误判为步骤失败。 st.mu.Lock() st.Plan.Workflow.Steps[idx] = step _ = mergeStepArtifacts(&st.BB, subDir) err = st.save() st.mu.Unlock() if err != nil { return nil, nil, err } d.emit(st, Event{Type: EvStepCompleted, StepID: step.ID, Role: step.Role, Text: step.Title, Data: marshalJSON(map[string]any{"duration_ms": step.DurationMs, "targets": targets})}) return nil, nil, nil } // failStep 按关键性处理失败步骤:关键 → 运行失败;非关键 → 标记后继续(返回 nil 让循环推进)。 func (d *flowDriver) failStep(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) { st.mu.Lock() step := st.Plan.Workflow.Steps[idx] critical := step.IsCritical() if critical { wf := st.Workflow() wf.Status = "failed" st.setWorkflow(wf) } _ = st.save() st.mu.Unlock() if critical { return &RunResult{ RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Error: fmt.Sprintf("step %s(%s) failed: %s", step.ID, step.Role, step.Error), WorkDir: st.Meta.WorkDir, }, nil, nil } // 非关键降级:标记 failed,工作流继续,收口时 degraded。 d.emit(st, Event{Type: EvStepSkipped, StepID: step.ID, Role: step.Role, Text: "非关键步骤失败,降级继续: " + step.Error}) return nil, nil, nil } // checkBlockingGaps 检查未解决阻塞缺口;interactive 挂起,auto 记录兜底假设并继续。 func (d *flowDriver) checkBlockingGaps(st *runState) *hitlGate { gaps := st.BB.UnresolvedBlockingGaps() if len(gaps) == 0 { return nil } mode := d.policy(st).mode() if mode == HitlAuto { for _, g := range gaps { st.BB.Assumptions = append(st.BB.Assumptions, AssumptionItem{ ID: fmt.Sprintf("asm_%d", len(st.BB.Assumptions)+1), Topic: g.ID, Statement: "无人值守模式:未获得人工输入,按现状继续推进(缺口保持未解决)", Rationale: g.Item, Source: "fallback", }) for i := range st.BB.Gaps { if st.BB.Gaps[i].ID == g.ID { st.BB.Gaps[i].Blocking = false } } } _ = st.save() return nil } if mode == HitlDisabled { return nil // 交由步骤级失败语义处理;缺口不再阻塞 } var lines []string for _, g := range gaps { lines = append(lines, fmt.Sprintf("- [%s] %s(owner=%s)", g.ID, g.Item, g.Owner)) } waitCond := waitConditionFrom(gaps) req := &HumanRequest{ ID: newRequestID(), RunID: st.Meta.ID, Kind: HitlGap, Severity: SeverityActionRequired, WaitCondition: waitCond, Question: fmt.Sprintf("发现 %d 个阻塞缺口,需要人工补充信息后才能继续:", len(gaps)), Options: []HumanOption{ {Value: "resolved", Label: "已在回答中补充信息,继续执行"}, {Value: "skip", Label: "跳过缺口,按现状继续"}, {Value: "abort", Label: "中止运行"}, }, Context: strings.Join(lines, "\n"), CreatedAt: time.Now(), Status: "pending", } gate := &hitlGate{Request: *req, PrevStatus: RunExecuting} if err := st.saveGate(gate); err == nil { st.Meta.Status = string(RunWaitingHuman) _ = st.save() d.emitHuman(st, req) } return gate } // applyDynamicAdjustment 评估步骤产出并应用/审批动态调整。 func (d *flowDriver) applyDynamicAdjustment(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) { step := st.Plan.Workflow.Steps[idx] subDir := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(step.SubWorkspace)) output := readFileOr(filepath.Join(subDir, firstTarget(step.Targets)), "") evalRes, err := d.opts.Evaluator.Evaluate(ctx, EvaluateRequest{ RunID: st.Meta.ID, Step: step, Workspace: st.Meta.WorkDir, Output: output, Blackboard: &st.BB, }) if err != nil || evalRes == nil { return nil, nil, err } for _, na := range evalRes.NewAssumptions { st.BB.Assumptions = append(st.BB.Assumptions, na) } for _, gapID := range evalRes.ResolvedGaps { st.BB.ResolveGapByID(gapID, "") } hasChanges := len(evalRes.DynamicStepsToAdd) > 0 || len(evalRes.StepsToSkip) > 0 if !hasChanges { return nil, nil, nil } if evalRes.NeedsApproval && d.policy(st).mode() == HitlInteractive { var addLines, skipLines []string for _, s := range evalRes.DynamicStepsToAdd { addLines = append(addLines, fmt.Sprintf("- %s(%s)%s", s.ID, s.Role, s.Title)) } for _, id := range evalRes.StepsToSkip { skipLines = append(skipLines, "- "+id) } req := &HumanRequest{ ID: newRequestID(), RunID: st.Meta.ID, StepID: step.ID, Kind: HitlAdjustment, Severity: SeverityAttention, Question: "评估器建议动态调整工作流,是否应用?", Options: []HumanOption{ {Value: "approve", Label: "应用调整"}, {Value: "reject", Label: "忽略调整"}, {Value: "abort", Label: "中止运行"}, }, Context: fmt.Sprintf("评估理由:%s\n追加步骤:\n%s\n跳过步骤:\n%s", evalRes.Reason, strings.Join(addLines, "\n"), strings.Join(skipLines, "\n")), CreatedAt: time.Now(), Status: "pending", } gate := &hitlGate{Request: *req, Steps: evalRes.DynamicStepsToAdd, SkipIDs: evalRes.StepsToSkip, PrevStatus: RunExecuting} if err := st.saveGate(gate); err == nil { st.Meta.Status = string(RunWaitingHuman) _ = st.save() d.emitHuman(st, req) } return heldResult(st, gate), gate, nil } applyAdjustment(st, evalRes.DynamicStepsToAdd, evalRes.StepsToSkip) return nil, nil, nil } // applyAdjustment 应用动态调整:新步骤插到最后一步之前,跳过步骤标记 skipped。 func applyAdjustment(st *runState, add []StepSpec, skipIDs []string) { steps := st.Plan.Workflow.Steps for _, id := range skipIDs { for i := range steps { if steps[i].ID == id && steps[i].Status == "pending" { steps[i].Status = "skipped" } } } if len(add) > 0 { insertAt := len(steps) if insertAt > 0 { insertAt-- // 插在最后(成文/终审)步骤之前 } for i := range add { add[i].Status = "pending" if add[i].Targets == nil { add[i].Targets = []string{"notes.md", "fields.json"} } } steps = append(steps[:insertAt], append(append([]StepSpec{}, add...), steps[insertAt:]...)...) } st.Plan.Workflow.Steps = steps _ = st.save() } // finalize 收口:汇总交付物 → 质检 → 自动修复 → (必要时)人工裁决 → 清单。 func (d *flowDriver) finalize(ctx context.Context, st *runState) (*RunResult, *hitlGate, error) { wf := st.Workflow() degraded := false for _, s := range wf.Steps { if s.Status == "failed" || s.Status == "skipped" { degraded = true } } // 汇总交付物:最后一个 completed 步骤的主产出 → output.md。 var srcStep *StepSpec for i := range wf.Steps { if wf.Steps[i].Status == "completed" { srcStep = &wf.Steps[i] } } if srcStep == nil { wf.Status = "failed" st.setWorkflow(wf) _ = st.save() return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Error: "no completed step to assemble deliverable", WorkDir: st.Meta.WorkDir}, nil, nil } src := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(srcStep.SubWorkspace), firstTarget(srcStep.Targets)) data, err := os.ReadFile(src) if err != nil { wf.Status = "failed" st.setWorkflow(wf) _ = st.save() return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Error: "deliverable source missing: " + err.Error(), WorkDir: st.Meta.WorkDir}, nil, nil } deliverable := "output.md" if err := os.WriteFile(filepath.Join(st.Meta.WorkDir, deliverable), data, 0o644); err != nil { return nil, nil, err } d.emit(st, Event{Type: EvDeliverable, Text: deliverable}) // 质检 + 有界自动修复。 report := d.opts.Checker.Check(CheckRequest{ RunID: st.Meta.ID, Workspace: st.Meta.WorkDir, Deliverable: deliverable, Plan: planPtr(st), Squad: &st.Plan.Squad, Template: st.Template, }) if !report.Passed && st.RepairBudgetRemaining > 0 { st.RepairBudgetRemaining-- _ = st.save() // 重跑交付物源步骤一次,再检一次。 if out, g, err := d.rerunForRepair(ctx, st, srcStep.ID); out != nil || g != nil || err != nil { return out, g, err } data2, _ := os.ReadFile(src) _ = os.WriteFile(filepath.Join(st.Meta.WorkDir, deliverable), data2, 0o644) report = d.opts.Checker.Check(CheckRequest{ RunID: st.Meta.ID, Workspace: st.Meta.WorkDir, Deliverable: deliverable, Plan: planPtr(st), Squad: &st.Plan.Squad, Template: st.Template, }) } if !report.Passed { switch d.policy(st).mode() { case HitlDisabled, HitlAuto: wf.Status = "failed" st.setWorkflow(wf) _ = st.save() return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Quality: report, Error: "quality check failed (hitl mode=" + string(d.policy(st).mode()) + "): " + strings.Join(report.Violations, "; "), WorkDir: st.Meta.WorkDir}, nil, nil default: req := &HumanRequest{ ID: newRequestID(), RunID: st.Meta.ID, Kind: HitlQualityReview, Severity: SeverityActionRequired, Question: "交付物质检未通过且自动修复预算已用尽,请人工裁决:", Options: []HumanOption{ {Value: "approve", Label: "人工放行(记录人工通过)"}, {Value: "retry", Label: "再修复一次(重跑交付步骤)"}, {Value: "abort", Label: "判定失败"}, }, Context: string(marshalJSON(report)), CreatedAt: time.Now(), Status: "pending", } gate := &hitlGate{Request: *req, PrevStatus: RunExecuting} if err := st.saveGate(gate); err == nil { st.Meta.Status = string(RunQualityReview) _ = st.save() d.emitHuman(st, req) } return heldResult(st, gate), gate, nil } } status := RunCompleted if degraded { status = RunDegraded } now := time.Now() wf.Status = string(status) st.setWorkflow(wf) st.Meta.Status = string(status) st.Meta.CompletedAt = &now _ = st.save() manifest := buildManifest(st, deliverable, *report) _ = writeJSON(filepath.Join(st.Meta.WorkDir, "manifest.json"), manifest) d.emit(st, Event{Type: EvRunCompleted, Text: string(status), Data: marshalJSON(map[string]any{"deliverable": deliverable, "score": report.Score})}) return &RunResult{ RunID: st.Meta.ID, Status: status, Plan: planPtr(st), Deliverable: deliverable, Quality: report, WorkDir: st.Meta.WorkDir, Usage: st.usageSnapshot(), Summary: summarize(st, *report, status), }, nil, nil } // rerunForRepair 重置交付物源步骤并重新执行到收口。 func (d *flowDriver) rerunForRepair(ctx context.Context, st *runState, stepID string) (*RunResult, *hitlGate, error) { for i := range st.Plan.Workflow.Steps { if st.Plan.Workflow.Steps[i].ID == stepID { st.Plan.Workflow.Steps[i].Status = "pending" st.Plan.Workflow.Steps[i].Error = "" } } d.emit(st, Event{Type: EvPhase, Text: "质检未通过,自动修复:重跑交付步骤 " + stepID}) return d.advance(ctx, st) } // heldResult 由挂起上下文构造对外结果(挂起路径统一返回非 nil 结果)。 func heldResult(st *runState, gate *hitlGate) *RunResult { status := RunWaitingHuman switch gate.Request.Kind { case HitlClarification: status = RunAwaitingClarification case HitlConfirmation: status = RunAwaitingConfirmation case HitlQualityReview: status = RunQualityReview } return &RunResult{ RunID: st.Meta.ID, Status: status, Plan: planPtr(st), Pending: &gate.Request, WorkDir: st.Meta.WorkDir, Usage: st.usageSnapshot(), } } // waitConditionFrom 从阻塞缺口提取等待条件(首个声明者胜出)。 func waitConditionFrom(gaps []GapItem) *WaitCondition { for _, g := range gaps { if g.WaitRunID != "" { return &WaitCondition{Type: "run_done", RunID: g.WaitRunID} } if g.WaitField != "" { return &WaitCondition{Type: "field_set", Field: g.WaitField} } } return nil } // hold 生成挂起请求并返回结果。 func (d *flowDriver) hold(st *runState, kind HumanRequestKind, stepID, role, question string, options []HumanOption, context string, extra *hitlGate) (*RunResult, *hitlGate, error) { req := &HumanRequest{ ID: newRequestID(), RunID: st.Meta.ID, StepID: stepID, Kind: kind, Severity: SeverityActionRequired, Question: question, Options: options, Context: context, CreatedAt: time.Now(), Status: "pending", } status := RunWaitingHuman switch kind { case HitlClarification: status = RunAwaitingClarification case HitlConfirmation: status = RunAwaitingConfirmation } gate := extra if gate == nil { gate = &hitlGate{} } gate.Request = *req gate.PrevStatus = RunExecuting if err := st.saveGate(gate); err != nil { return nil, nil, err } st.Meta.Status = string(status) _ = st.save() d.emitHuman(st, req) return &RunResult{RunID: st.Meta.ID, Status: status, Plan: planPtr(st), Pending: req, WorkDir: st.Meta.WorkDir}, nil, nil } // policy 合并引擎全局策略与 Goal 覆盖(窄者胜)。 func (d *flowDriver) policy(st *runState) HitlPolicy { return mergeHitlPolicy(d.opts.HitlPolicy, st.Meta.HitlPolicyOf()) } func (d *flowDriver) emit(st *runState, ev Event) { if d.opts.Sink == nil { return } ev.RunID = st.Meta.ID if ev.At.IsZero() { ev.At = time.Now() } d.opts.Sink.Emit(ev) } func (d *flowDriver) emitHuman(st *runState, req *HumanRequest) { d.emit(st, Event{Type: EvHumanInputRequired, Text: req.Question, Data: marshalJSON(req)}) } func summarize(st *runState, report QualityReport, status RunStatus) string { var b strings.Builder fmt.Fprintf(&b, "运行 %s:%s。步骤 %d 个(完成 %d/跳过 %d/失败 %d)。质检得分 %.0f。", st.Meta.ID, status, len(st.Plan.Workflow.Steps), countStatus(st, "completed"), countStatus(st, "skipped"), countStatus(st, "failed"), report.Score) if gaps := st.BB.UnresolvedBlockingGaps(); len(gaps) > 0 { fmt.Fprintf(&b, "未解决缺口 %d 个。", len(gaps)) } return b.String() } func countStatus(st *runState, status string) int { n := 0 for _, s := range st.Plan.Workflow.Steps { if s.Status == status { n++ } } return n } func buildManifest(st *runState, deliverable string, report QualityReport) Manifest { files := map[string]string{} _ = filepath.Walk(st.Meta.WorkDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } rel, _ := filepath.Rel(st.Meta.WorkDir, path) files[filepath.ToSlash(rel)] = sha256File(path) return nil }) start := st.Meta.CreatedAt return Manifest{ ManifestVersion: "1", WorkspaceID: st.Meta.ID, GeneratedAt: time.Now(), Deliverable: deliverable, Files: files, QualityReport: report, DurationMs: time.Since(start).Milliseconds(), } } func firstTarget(targets []string) string { if len(targets) > 0 { return targets[0] } return "output.md" } func allTargetsPresent(dir string, targets []string) bool { return len(missingTargets(dir, targets)) == 0 } func missingTargets(dir string, targets []string) []string { var missing []string for _, t := range targets { if strings.TrimSpace(t) == "" { continue } info, err := os.Stat(filepath.Join(dir, filepath.FromSlash(t))) if err != nil || info.Size() == 0 { missing = append(missing, t) } } return missing } func readFileOr(path, fallback string) string { if data, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(data))) > 0 { return string(data) } return fallback } func appendLog(dir, line string) { f, err := os.OpenFile(filepath.Join(dir, "execution.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return } defer f.Close() _, _ = f.WriteString(line) } func planPtr(st *runState) *WorkflowPlan { wf := st.Workflow() return &wf } func orDefault(v, def string) string { if v == "" { return def } return v }