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,286 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const dataDir = ".crows"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "run":
|
||||
cmdRun(os.Args[2:])
|
||||
case "trust":
|
||||
cmdTrust()
|
||||
case "log":
|
||||
cmdLog(os.Args[2:])
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `So Many Crows
|
||||
|
||||
crows run <todo.txt> [-as name] hand the flock a todo list
|
||||
crows trust show trust ledger
|
||||
crows log [-n 20] show recent flight log entries`)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
type itemResult struct {
|
||||
Item, Mode, TaskType, Answer, Verdict, Skipped string
|
||||
Crows []string
|
||||
}
|
||||
|
||||
func cmdRun(args []string) {
|
||||
fs := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
as := fs.String("as", defaultRequester(), "requester id")
|
||||
var pos []string
|
||||
for fs.Parse(args); fs.NArg() > 0; fs.Parse(args) {
|
||||
pos = append(pos, fs.Arg(0))
|
||||
args = fs.Args()[1:]
|
||||
}
|
||||
if len(pos) != 1 {
|
||||
usage()
|
||||
}
|
||||
todoPath := pos[0]
|
||||
g := DefaultGuardrails()
|
||||
|
||||
ledger, err := LoadLedger(filepath.Join(dataDir, "ledger.json"))
|
||||
must(err)
|
||||
flog, err := OpenFlightLog(filepath.Join(dataDir, "flight.jsonl"))
|
||||
must(err)
|
||||
defer flog.Close()
|
||||
save := func() {
|
||||
if err := ledger.Save(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "could not save ledger:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ok, msg := ledger.Admit(*as, time.Now(), g); !ok {
|
||||
fmt.Println(msg)
|
||||
flog.Write(Event{Kind: "denied", Requester: *as, Note: msg})
|
||||
save()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
items, err := readTodo(todoPath, g)
|
||||
must(err)
|
||||
if len(items) > g.MaxItemsPerList {
|
||||
fmt.Printf("That list has %d items. The flock takes at most %d at a time.\n", len(items), g.MaxItemsPerList)
|
||||
save()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
brain := NewBrain()
|
||||
flock := NewFlock(brain, g, flog)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
listBudget := &Budget{name: "list", limit: g.TokenBudgetPerList}
|
||||
|
||||
fmt.Printf("Brain: %s · requester: %s · %d items\n", brain.Name(), *as, len(items))
|
||||
flog.Write(Event{Kind: "list", Requester: *as, Note: fmt.Sprintf("%s (%d items)", todoPath, len(items))})
|
||||
|
||||
var results []itemResult
|
||||
for i, item := range items {
|
||||
fmt.Printf("\n[%d/%d] %s\n", i+1, len(items), item)
|
||||
r := itemResult{Item: item}
|
||||
itemBudget := &Budget{name: "item", limit: g.TokenBudgetPerItem}
|
||||
|
||||
t, err := flock.Triage(ctx, item, itemBudget, listBudget)
|
||||
if err != nil {
|
||||
r.Skipped = "triage failed: " + err.Error()
|
||||
fmt.Println(" " + r.Skipped)
|
||||
results = append(results, r)
|
||||
continue
|
||||
}
|
||||
r.Mode, r.TaskType = t.Mode, t.TaskType
|
||||
fmt.Printf(" Sentry: %s · %s · %s\n", t.Mode, t.TaskType, t.Reason)
|
||||
for _, s := range t.Subtasks {
|
||||
fmt.Printf(" - %s\n", s)
|
||||
}
|
||||
flog.Write(Event{Kind: "triage", Requester: *as, Item: item, Mode: t.Mode, TaskType: t.TaskType, Note: t.Reason})
|
||||
|
||||
// The Sentry earns autonomy on triage separately from the crows' work.
|
||||
triageType := "triage/" + t.TaskType
|
||||
if ledger.Level("Sentry", triageType) < Trusted {
|
||||
ok := confirm(in, " Approve triage?")
|
||||
ledger.Judge([]string{"Sentry"}, triageType, ok, g.PromoteAfter)
|
||||
flog.Write(Event{Kind: "verdict", Item: item, Crow: "Sentry", TaskType: triageType, Verdict: verdict(ok)})
|
||||
if !ok {
|
||||
r.Skipped = "triage rejected"
|
||||
results = append(results, r)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
out, err := flock.Execute(ctx, item, t, itemBudget, listBudget)
|
||||
if err != nil {
|
||||
r.Skipped = "flight failed: " + err.Error()
|
||||
fmt.Println(" " + r.Skipped)
|
||||
results = append(results, r)
|
||||
continue
|
||||
}
|
||||
r.Answer, r.Crows = out.Answer, out.Crows
|
||||
fmt.Printf(" Crows: %s\n%s\n", strings.Join(out.Crows, ", "), indent(out.Answer, " > "))
|
||||
|
||||
// The least-trusted crow involved sets the bar.
|
||||
lowest := Trusted
|
||||
for _, c := range out.Crows {
|
||||
lowest = min(lowest, ledger.Level(c, t.TaskType))
|
||||
}
|
||||
if lowest < Regular {
|
||||
ok := confirm(in, " Accept this answer?")
|
||||
ledger.Judge(out.Crows, t.TaskType, ok, g.PromoteAfter)
|
||||
r.Verdict = verdict(ok)
|
||||
} else if lowest < Trusted {
|
||||
r.Verdict = "accepted, unreviewed"
|
||||
} else {
|
||||
r.Verdict = "accepted, trusted"
|
||||
}
|
||||
flog.Write(Event{Kind: "verdict", Item: item, Crow: strings.Join(out.Crows, ","), TaskType: t.TaskType, Verdict: r.Verdict})
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
save()
|
||||
resultPath := strings.TrimSuffix(todoPath, filepath.Ext(todoPath)) + ".crows.md"
|
||||
must(writeResults(resultPath, todoPath, results))
|
||||
fmt.Printf("\nResults: %s · tokens used: %d/%d\n", resultPath, listBudget.spent, listBudget.limit)
|
||||
}
|
||||
|
||||
func readTodo(path string, g Guardrails) ([]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
for _, p := range []string{"- [ ]", "- [x]", "-", "*"} {
|
||||
line = strings.TrimSpace(strings.TrimPrefix(line, p))
|
||||
}
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if len(line) > g.MaxItemChars {
|
||||
return nil, fmt.Errorf("an item is %d characters; the cap is %d", len(line), g.MaxItemChars)
|
||||
}
|
||||
items = append(items, line)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// confirm fails safe: anything but y/yes, including no input at all, is a no.
|
||||
func confirm(in *bufio.Reader, prompt string) bool {
|
||||
fmt.Print(prompt + " [y/N] ")
|
||||
line, _ := in.ReadString('\n')
|
||||
a := strings.ToLower(strings.TrimSpace(line))
|
||||
if line == "" {
|
||||
fmt.Println()
|
||||
}
|
||||
return a == "y" || a == "yes"
|
||||
}
|
||||
|
||||
func verdict(ok bool) string {
|
||||
if ok {
|
||||
return "approved"
|
||||
}
|
||||
return "rejected"
|
||||
}
|
||||
|
||||
func writeResults(path, source string, results []itemResult) error {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "# So Many Crows: %s\n\n_%s_\n\n", filepath.Base(source), time.Now().Format("2006-01-02 15:04"))
|
||||
for _, r := range results {
|
||||
if r.Skipped != "" {
|
||||
fmt.Fprintf(&sb, "- [ ] %s\n - skipped: %s\n\n", r.Item, r.Skipped)
|
||||
continue
|
||||
}
|
||||
box := "x"
|
||||
if r.Verdict == "rejected" {
|
||||
box = " "
|
||||
}
|
||||
fmt.Fprintf(&sb, "- [%s] %s\n - %s · %s · %s · %s\n%s\n\n", box, r.Item, r.Mode, r.TaskType, strings.Join(r.Crows, ", "), r.Verdict, indent(r.Answer, " > "))
|
||||
}
|
||||
return os.WriteFile(path, []byte(sb.String()), 0o644)
|
||||
}
|
||||
|
||||
func cmdTrust() {
|
||||
l, err := LoadLedger(filepath.Join(dataDir, "ledger.json"))
|
||||
must(err)
|
||||
names := []string{"stranger", "regular", "trusted"}
|
||||
show := func(title string, m map[string]*Record) {
|
||||
fmt.Println(title)
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
if len(keys) == 0 {
|
||||
fmt.Println(" (none yet)")
|
||||
}
|
||||
for _, k := range keys {
|
||||
r := m[k]
|
||||
fmt.Printf(" %-28s %-9s +%d -%d streak %d\n", k, names[r.Level], r.Approved, r.Rejected, r.Streak)
|
||||
}
|
||||
}
|
||||
show("Crows", l.Crows)
|
||||
show("\nFlock", l.Flock)
|
||||
fmt.Println("\nRequesters")
|
||||
for id, r := range l.Requesters {
|
||||
cd := "none"
|
||||
if time.Now().Before(r.CooldownUntil) {
|
||||
cd = time.Until(r.CooldownUntil).Round(time.Second).String()
|
||||
}
|
||||
fmt.Printf(" %-20s strikes %d · cooldown %s\n", id, r.Strikes, cd)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdLog(args []string) {
|
||||
fs := flag.NewFlagSet("log", flag.ExitOnError)
|
||||
n := fs.Int("n", 20, "entries to show")
|
||||
fs.Parse(args)
|
||||
data, err := os.ReadFile(filepath.Join(dataDir, "flight.jsonl"))
|
||||
must(err)
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if len(lines) > *n {
|
||||
lines = lines[len(lines)-*n:]
|
||||
}
|
||||
fmt.Println(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func indent(s, prefix string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
for i := range lines {
|
||||
lines[i] = prefix + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func defaultRequester() string {
|
||||
if u := os.Getenv("USER"); u != "" {
|
||||
return u
|
||||
}
|
||||
return "local"
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user