From b4d9621d891d3d69a1108b0e7ee3ba3341e1acd1 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 26 Jul 2026 20:35:57 +0400 Subject: [PATCH] schedule daily standup advisories --- cmd/orchestra/main.go | 41 +++++++++++++++++++++++++++++++ internal/operations/operations.go | 16 ++++++++++++ internal/store/store.go | 6 ++++- progress.md | 2 ++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cmd/orchestra/main.go b/cmd/orchestra/main.go index 3bc1f54..c21ef95 100644 --- a/cmd/orchestra/main.go +++ b/cmd/orchestra/main.go @@ -172,6 +172,31 @@ func main() { } json.NewEncoder(w).Encode(operations.BuildBrief(s.Events(0), from, to, operations.GitState(dir))) }) + standup := func() (domain.Event, error) { + items, _ := json.Marshal(map[string]any{"items": operations.StandupItems(s.Tasks()), "generated_at": time.Now().UTC()}) + e := domain.Event{ID: id(), Type: "StandupAdvisory", TaskID: "system", Version: 0, Payload: items} + return e, s.Append(e) + } + mux.HandleFunc("/v1/standup", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + json.NewEncoder(w).Encode(operations.StandupItems(s.Tasks())) + return + } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", 405) + return + } + if err := authz.AuthorizeEvent(surface(r), "StandupAdvisory"); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + e, err := standup() + if err != nil { + http.Error(w, err.Error(), 400) + return + } + json.NewEncoder(w).Encode(e) + }) mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { tasks := s.Tasks() counts := map[domain.TaskState]int{} @@ -428,6 +453,22 @@ func main() { } }() } + go func() { + lastDay := "" + t := time.NewTicker(time.Minute) + defer t.Stop() + for now := range t.C { + utc := now.UTC() + day := utc.Format("2006-01-02") + if utc.Hour() == 3 && day != lastDay { + if _, err := standup(); err != nil { + log.Printf("standup advisory: %v", err) + } else { + lastDay = day + } + } + } + }() port := os.Getenv("ORCHESTRA_PORT") if port == "" { port = "9145" diff --git a/internal/operations/operations.go b/internal/operations/operations.go index 5d51182..c2bae0e 100644 --- a/internal/operations/operations.go +++ b/internal/operations/operations.go @@ -23,6 +23,22 @@ type GitSync struct { Branch, Head, Status string `json:"branch" json:"head" json:"status"` } +type StandupItem struct { + TaskID string `json:"task_id"` + State domain.TaskState `json:"state"` + Title string `json:"title,omitempty"` +} + +func StandupItems(tasks []domain.Task) []StandupItem { + out := make([]StandupItem, 0) + for _, t := range tasks { + if t.State == domain.StateQueued || t.State == domain.StateLeased || t.State == domain.StateBlocked { + out = append(out, StandupItem{t.ID, t.State, t.Title}) + } + } + return out +} + // BuildBrief folds only events in [from,to]. It is deliberately read-only. func BuildBrief(events []domain.Event, from, to time.Time, git GitSync) Brief { b := Brief{From: from, To: to, Quota: map[string]float64{}, Git: git} diff --git a/internal/store/store.go b/internal/store/store.go index 7a727c4..7b13a01 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -94,6 +94,9 @@ func (s *Store) apply(e domain.Event) error { return err } t := s.tasks[e.TaskID] + if e.Type == "QuotaReported" || e.Type == "StandupAdvisory" { + return nil + } switch e.Type { case "TaskCreated": if err := domain.ValidateCreated(p); err != nil { @@ -193,7 +196,8 @@ func (s *Store) Append(e domain.Event) error { return domain.ErrConflict } } - if !taskExists && e.Type != "TaskCreated" { + global := e.Type == "QuotaReported" || e.Type == "StandupAdvisory" + if !taskExists && e.Type != "TaskCreated" && !global { return domain.ErrNotFound } if e.Type != "TaskCreated" && (e.Type == "TaskCompleted" || e.Type == "TaskBlocked" || e.Type == "TaskReleased") { diff --git a/progress.md b/progress.md index 6f49092..301212a 100644 --- a/progress.md +++ b/progress.md @@ -73,6 +73,8 @@ Federation control-plane foundations now include worker registration, heartbeat Configured herdr `quota_limit` values are now wired into router availability using the rolling-window 80% conservative filter; previously the quota implementation existed but was not active in server routing. +Quota/standup scheduling now treats `QuotaReported` and `StandupAdvisory` as global events, adds `/v1/standup` read/request behavior, and emits one daily advisory during the 03:00 UTC safety window. Advisory contents include queued, leased, and blocked tasks. + Recommended order: 1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.