So Many Crows v1: Sentry triage, crows, guardrails, trust ledger
Go command-line tool. Sentry classifies each todo item as solo, split, or murder; text-only crows fly under budget, timeout, and flock caps; trust is earned per crow and per task type; abusive requesters get a mild, fading cooldown. Runs offline with a mock brain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
10 files changed
+999
No files matched your search
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Trust levels.
|
||||
const (
|
||||
Stranger = 0 // you approve triage and output
|
||||
Regular = 1 // output auto-accepted, marked unreviewed
|
||||
Trusted = 2 // runs on its own
|
||||
)
|
||||
|
||||
// Record is a track record for one holder (a crow, or the whole flock) on one task type.
|
||||
type Record struct {
|
||||
Approved int `json:"approved"`
|
||||
Rejected int `json:"rejected"`
|
||||
Streak int `json:"streak"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
func (r *Record) total() int { return r.Approved + r.Rejected }
|
||||
|
||||
// apply: slow to earn, quick to lose.
|
||||
func (r *Record) apply(ok bool, promoteAfter int) {
|
||||
if ok {
|
||||
r.Approved++
|
||||
r.Streak++
|
||||
if r.Streak >= promoteAfter && r.Level < Trusted {
|
||||
r.Level++
|
||||
r.Streak = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
r.Rejected++
|
||||
r.Streak = 0
|
||||
if r.Level > Stranger {
|
||||
r.Level--
|
||||
}
|
||||
}
|
||||
|
||||
// Requester trust: abuse earns a mild, fading penalty.
|
||||
type Requester struct {
|
||||
Strikes int `json:"strikes"`
|
||||
LastStrike time.Time `json:"last_strike"`
|
||||
CooldownUntil time.Time `json:"cooldown_until"`
|
||||
Recent []time.Time `json:"recent"`
|
||||
}
|
||||
|
||||
type Ledger struct {
|
||||
Crows map[string]*Record `json:"crows"` // key: crow|task_type
|
||||
Flock map[string]*Record `json:"flock"` // key: task_type
|
||||
Requesters map[string]*Requester `json:"requesters"`
|
||||
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func LoadLedger(path string) (*Ledger, error) {
|
||||
l := &Ledger{path: path}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(data, l); err != nil {
|
||||
return nil, fmt.Errorf("ledger is unreadable: %w", err)
|
||||
}
|
||||
}
|
||||
if l.Crows == nil {
|
||||
l.Crows = map[string]*Record{}
|
||||
}
|
||||
if l.Flock == nil {
|
||||
l.Flock = map[string]*Record{}
|
||||
}
|
||||
if l.Requesters == nil {
|
||||
l.Requesters = map[string]*Requester{}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l *Ledger) Save() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
data, err := json.MarshalIndent(l, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(l.path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := l.path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, l.path)
|
||||
}
|
||||
|
||||
func get(m map[string]*Record, k string) *Record {
|
||||
r := m[k]
|
||||
if r == nil {
|
||||
r = &Record{}
|
||||
m[k] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Level blends a crow's own record with the flock's record for the task type.
|
||||
// A new crow flies on the flock's reputation; its own record takes over as it grows.
|
||||
func (l *Ledger) Level(crow, taskType string) int {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
own := get(l.Crows, crow+"|"+taskType)
|
||||
flock := get(l.Flock, taskType)
|
||||
w := float64(own.total()) / float64(own.total()+5)
|
||||
return int(math.Round(w*float64(own.Level) + (1-w)*float64(flock.Level)))
|
||||
}
|
||||
|
||||
// Judge records a verdict for every crow involved, and once for the flock.
|
||||
// The flock needs twice the streak to level up.
|
||||
func (l *Ledger) Judge(crows []string, taskType string, ok bool, promoteAfter int) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
for _, c := range crows {
|
||||
get(l.Crows, c+"|"+taskType).apply(ok, promoteAfter)
|
||||
}
|
||||
get(l.Flock, taskType).apply(ok, promoteAfter*2)
|
||||
}
|
||||
|
||||
func (l *Ledger) requester(id string) *Requester {
|
||||
r := l.Requesters[id]
|
||||
if r == nil {
|
||||
r = &Requester{}
|
||||
l.Requesters[id] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Admit checks a requester at the door: cooldowns, rate cap, and fading grudges.
|
||||
func (l *Ledger) Admit(id string, now time.Time, g Guardrails) (bool, string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
r := l.requester(id)
|
||||
|
||||
// Grudges fade: one strike forgiven per clean day.
|
||||
if r.Strikes > 0 {
|
||||
if days := int(now.Sub(r.LastStrike) / (24 * time.Hour)); days > 0 {
|
||||
r.Strikes = max(0, r.Strikes-days)
|
||||
r.LastStrike = r.LastStrike.Add(time.Duration(days) * 24 * time.Hour)
|
||||
}
|
||||
}
|
||||
if now.Before(r.CooldownUntil) {
|
||||
return false, prank(r.CooldownUntil.Sub(now))
|
||||
}
|
||||
kept := r.Recent[:0]
|
||||
for _, t := range r.Recent {
|
||||
if now.Sub(t) < time.Minute {
|
||||
kept = append(kept, t)
|
||||
}
|
||||
}
|
||||
r.Recent = kept
|
||||
if len(r.Recent) >= g.MaxRequestsPerMin {
|
||||
return false, prank(strike(r, now, g))
|
||||
}
|
||||
r.Recent = append(r.Recent, now)
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func strike(r *Requester, now time.Time, g Guardrails) time.Duration {
|
||||
r.Strikes++
|
||||
r.LastStrike = now
|
||||
cd := g.BaseCooldown
|
||||
for i := 1; i < r.Strikes && cd < g.MaxCooldown; i++ {
|
||||
cd *= 2
|
||||
}
|
||||
cd = min(cd, g.MaxCooldown)
|
||||
r.CooldownUntil = now.Add(cd)
|
||||
return cd
|
||||
}
|
||||
|
||||
var pranks = []string{
|
||||
"A crow has left a small gift on your windshield. The flock will hear you again in %s.",
|
||||
"The flock turns its back on you. Try again in %s.",
|
||||
"Something shiny of yours has gone missing. Come back in %s.",
|
||||
"Caw. (Translation: not for another %s.)",
|
||||
}
|
||||
|
||||
func prank(d time.Duration) string {
|
||||
return fmt.Sprintf(pranks[rand.IntN(len(pranks))], d.Round(time.Second))
|
||||
}
|
||||
Reference in new issue
Block a user