package report import ( "context" "encoding/json" "fmt" "sync" "g.fc.run/misc/byre/internal/mcp" ) const maxNudges = 3 const nudge = "You must call the report tool (mcp__byre__report) before ending your turn." type Report struct { Done []string `json:"done"` OpenQuestions []string `json:"open_questions"` NextSteps []string `json:"next_steps"` Risks []string `json:"risks"` Deviations []string `json:"deviations"` } type Gate struct { OnReport func(Report) mu sync.Mutex reported bool nudges int } func (g *Gate) Reset() { g.mu.Lock() defer g.mu.Unlock() g.reported = false g.nudges = 0 } func (g *Gate) Nudges() int { g.mu.Lock() defer g.mu.Unlock() return g.nudges } func (g *Gate) Tool() mcp.Tool { return mcp.Tool{ Name: "report", Description: "Report what you did this turn. You must call this before ending every turn. List every departure from the plan in deviations.", InputSchema: json.RawMessage(`{ "type": "object", "properties": { "done": {"type": "array", "items": {"type": "string"}}, "open_questions": {"type": "array", "items": {"type": "string"}}, "next_steps": {"type": "array", "items": {"type": "string"}}, "risks": {"type": "array", "items": {"type": "string"}}, "deviations": {"type": "array", "items": {"type": "string"}} }, "required": ["done"] }`), Handler: g.handle, } } func (g *Gate) handle(ctx context.Context, args json.RawMessage) (string, error) { r := Report{} err := json.Unmarshal(args, &r) if err != nil { return "", fmt.Errorf("malformed report: %w", err) } if len(r.Done) == 0 { return "", fmt.Errorf("report must list at least one item in done") } g.mu.Lock() g.reported = true g.mu.Unlock() if g.OnReport != nil { g.OnReport(r) } return "report filed", nil } func (g *Gate) StopHook(ctx context.Context, input json.RawMessage) (map[string]any, error) { g.mu.Lock() defer g.mu.Unlock() if g.reported || g.nudges >= maxNudges { return map[string]any{}, nil } g.nudges++ return map[string]any{"decision": "block", "reason": nudge}, nil }