Files
Agent-Squad/squad/blueprint.go
weichenandCursor 8e987bf535
ci / vet-and-test (pull_request) Failing after 1s
ci / docker-checkout-hostgateway (pull_request) Failing after 3s
Persist step approvals and guard squad HTTP entry points
Why: A step confirmation cancelled the whole run, approvals and repair budget vanished on restart, and the HTTP server accepted anonymous and oversized requests.
Evidence: GOWORK=off go vet ./... && GOWORK=off go test -count=1 ./... && GOWORK=off go test -race ./squad/... passed. scripts/submodule-consistency.sh ok.
Impact: Approve and skip apply to one step; runstate.json and unix flock survive restart; wakeup leaves permission holds pending; serve defaults to loopback and requires a token off-loopback.
Refs: Q1 Q2 Q3 Q4 Q5 Q8 Q9 Q14 Q15 Q16
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 01:48:10 +00:00

104 lines
2.4 KiB
Go

package squad
import (
"fmt"
"sort"
"strings"
"sync"
)
// BlueprintRegistry 任务模板注册中心:空表起步。
// 命中模板的 Goal 走基线工作流,未命中走规划器自主拆解。
type BlueprintRegistry struct {
mu sync.RWMutex
bps map[string]Blueprint
// OnChange 可选钩子:注册/删除后回调(store 层用于落盘)。
OnChange func(bp Blueprint) error
}
// NewBlueprintRegistry 创建空注册表。
func NewBlueprintRegistry() *BlueprintRegistry {
return &BlueprintRegistry{bps: make(map[string]Blueprint)}
}
// Register 注册或更新模板。
func (r *BlueprintRegistry) Register(bp Blueprint) error {
if !ValidID(bp.Key) {
return invalidID(bp.Key)
}
if len(bp.Steps) == 0 {
return fmt.Errorf("blueprint %s has no steps", bp.Key)
}
for i := range bp.Steps {
if bp.Steps[i].ID == "" {
bp.Steps[i].ID = fmt.Sprintf("step_%02d", i+1)
}
if bp.Steps[i].Targets == nil {
bp.Steps[i].Targets = []string{"output.md"}
}
}
r.mu.Lock()
r.bps[bp.Key] = bp
r.mu.Unlock()
if r.OnChange != nil {
if err := r.OnChange(bp); err != nil {
return err
}
}
return nil
}
// Get 按 Key 获取模板。
func (r *BlueprintRegistry) Get(key string) (Blueprint, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
bp, ok := r.bps[key]
return bp, ok
}
// List 列出全部模板(按 Key 排序)。
func (r *BlueprintRegistry) List() []Blueprint {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]Blueprint, 0, len(r.bps))
for _, bp := range r.bps {
out = append(out, bp)
}
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
return out
}
// Remove 删除模板。
func (r *BlueprintRegistry) Remove(key string) bool {
r.mu.Lock()
_, ok := r.bps[key]
if ok {
delete(r.bps, key)
}
r.mu.Unlock()
return ok
}
// Match 按目标文本匹配模板:命中 Match 关键词数最多者;无命中返回 false。
// 显式 taskKey 非空时直接按 Key 精确查找。
func (r *BlueprintRegistry) Match(goalText, taskKey string) (Blueprint, bool) {
if taskKey != "" {
return r.Get(taskKey)
}
q := strings.ToLower(goalText)
var best Blueprint
bestScore := 0
for _, bp := range r.List() {
score := 0
for _, kw := range bp.Match {
if kw != "" && strings.Contains(q, strings.ToLower(kw)) {
score++
}
}
if score > bestScore {
best, bestScore = bp, score
}
}
return best, bestScore > 0
}