schedule daily standup advisories
This commit is contained in:
@@ -172,6 +172,31 @@ func main() {
|
|||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(operations.BuildBrief(s.Events(0), from, to, operations.GitState(dir)))
|
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) {
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||||
tasks := s.Tasks()
|
tasks := s.Tasks()
|
||||||
counts := map[domain.TaskState]int{}
|
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")
|
port := os.Getenv("ORCHESTRA_PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "9145"
|
port = "9145"
|
||||||
|
|||||||
@@ -23,6 +23,22 @@ type GitSync struct {
|
|||||||
Branch, Head, Status string `json:"branch" json:"head" json:"status"`
|
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.
|
// BuildBrief folds only events in [from,to]. It is deliberately read-only.
|
||||||
func BuildBrief(events []domain.Event, from, to time.Time, git GitSync) Brief {
|
func BuildBrief(events []domain.Event, from, to time.Time, git GitSync) Brief {
|
||||||
b := Brief{From: from, To: to, Quota: map[string]float64{}, Git: git}
|
b := Brief{From: from, To: to, Quota: map[string]float64{}, Git: git}
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ func (s *Store) apply(e domain.Event) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t := s.tasks[e.TaskID]
|
t := s.tasks[e.TaskID]
|
||||||
|
if e.Type == "QuotaReported" || e.Type == "StandupAdvisory" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case "TaskCreated":
|
case "TaskCreated":
|
||||||
if err := domain.ValidateCreated(p); err != nil {
|
if err := domain.ValidateCreated(p); err != nil {
|
||||||
@@ -193,7 +196,8 @@ func (s *Store) Append(e domain.Event) error {
|
|||||||
return domain.ErrConflict
|
return domain.ErrConflict
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !taskExists && e.Type != "TaskCreated" {
|
global := e.Type == "QuotaReported" || e.Type == "StandupAdvisory"
|
||||||
|
if !taskExists && e.Type != "TaskCreated" && !global {
|
||||||
return domain.ErrNotFound
|
return domain.ErrNotFound
|
||||||
}
|
}
|
||||||
if e.Type != "TaskCreated" && (e.Type == "TaskCompleted" || e.Type == "TaskBlocked" || e.Type == "TaskReleased") {
|
if e.Type != "TaskCreated" && (e.Type == "TaskCompleted" || e.Type == "TaskBlocked" || e.Type == "TaskReleased") {
|
||||||
|
|||||||
@@ -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.
|
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:
|
Recommended order:
|
||||||
|
|
||||||
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.
|
||||||
|
|||||||
Reference in New Issue
Block a user