Nests: team rooms with core crows and borrowing

- Server: nests API, persistence, core-first picking, borrowed flag
  (built by the backend agent against EVENTS.md; tests pass with -race)
- UI: nests in the sidebar, one feed per nest, header shows the core
  team, New nest dialog, borrowed crows labeled, per-nest waiting count
- start.command: one-click local server in controlled mode (mock crows,
  localhost only, state in .crows/)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
SmokeyandClaude Opus 5.5 committed 2026-10-27 13:45:04 -06:00
1 parent e9f20349a1
commit 2d85cab716
14 files changed
+782 -62

No files matched your search

+44 -4
View File
@@ -31,12 +31,13 @@ type Server struct {
p *Pipeline
approver *WebApprover
hub *Hub
nests *NestStore
ctx context.Context // cancelled on shutdown; lists and streams end with it
beat time.Duration
}
func NewServer(ctx context.Context, p *Pipeline, approver *WebApprover) *Server {
return &Server{p: p, approver: approver, hub: NewHub(), ctx: ctx, beat: heartbeat}
func NewServer(ctx context.Context, p *Pipeline, approver *WebApprover, nests *NestStore) *Server {
return &Server{p: p, approver: approver, hub: NewHub(), nests: nests, ctx: ctx, beat: heartbeat}
}
func (s *Server) Handler() http.Handler {
@@ -45,6 +46,8 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/events", s.events)
mux.HandleFunc("POST /api/verdicts", s.postVerdict)
mux.HandleFunc("GET /api/trust", s.trust)
mux.HandleFunc("GET /api/nests", s.getNests)
mux.HandleFunc("POST /api/nests", s.postNest)
if st, err := os.Stat(webDir); err == nil && st.IsDir() {
mux.Handle("GET /", http.FileServer(http.Dir(webDir)))
} else {
@@ -67,13 +70,15 @@ func cmdServe(args []string) {
flog, err := OpenFlightLog(filepath.Join(dataDir, "flight.jsonl"))
must(err)
defer flog.Close()
nests, err := LoadNests(filepath.Join(dataDir, "nests.json"))
must(err)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
brain := NewBrain()
approver := NewWebApprover(*timeout)
p := &Pipeline{Flock: NewFlock(brain, g, flog), Ledger: ledger, Log: flog, G: g, Approver: approver}
s := NewServer(ctx, p, approver)
s := NewServer(ctx, p, approver, nests)
srv := &http.Server{Addr: *addr, Handler: s.Handler(), ReadHeaderTimeout: 10 * time.Second}
go func() {
@@ -120,10 +125,19 @@ func (s *Server) postList(w http.ResponseWriter, r *http.Request) {
var body struct {
Text string `json:"text"`
Requester string `json:"requester"`
NestID string `json:"nest_id"`
}
if !decode(w, r, &body) {
return
}
if body.NestID == "" {
body.NestID = defaultNestID
}
nest, found := s.nests.Get(body.NestID)
if !found {
httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", body.NestID))
return
}
if body.Requester == "" {
body.Requester, _, _ = net.SplitHostPort(r.RemoteAddr)
}
@@ -157,7 +171,7 @@ func (s *Server) postList(w http.ResponseWriter, r *http.Request) {
id := newListID()
s.hub.Open(id, body.Requester)
l := List{ID: id, Requester: body.Requester, Source: "web", Items: items, Parallel: true}
l := List{ID: id, Requester: body.Requester, Source: "web", Items: items, Nest: nest, Parallel: true}
go s.p.Run(s.ctx, l, s.hub.Emitter(id))
writeJSON(w, http.StatusAccepted, map[string]string{"list_id": id})
}
@@ -253,3 +267,29 @@ func (s *Server) trust(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (s *Server) getNests(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"roster": roster, "nests": s.nests.All()})
}
func (s *Server) postNest(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
Crows []string `json:"crows"`
}
if !decode(w, r, &body) {
return
}
n, err := s.nests.Create(body.Name, body.Crows)
var bad *NestError
switch {
case errors.As(err, &bad) && bad.Clash:
httpError(w, http.StatusConflict, bad.Msg)
case errors.As(err, &bad):
httpError(w, http.StatusBadRequest, bad.Msg)
case err != nil:
httpError(w, http.StatusInternalServerError, "could not save the nest: "+err.Error())
default:
writeJSON(w, http.StatusCreated, n)
}
}