diff --git a/cmd/orchestra/main.go b/cmd/orchestra/main.go index 33724a5..62893d6 100644 --- a/cmd/orchestra/main.go +++ b/cmd/orchestra/main.go @@ -221,27 +221,21 @@ func main() { } types := map[string]string{"release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked"} var p map[string]any - if r.Body != nil { - if json.NewDecoder(r.Body).Decode(&p) != nil { - p = map[string]any{} - } - } - if p == nil { - p = map[string]any{} + if r.Body == nil || json.NewDecoder(r.Body).Decode(&p) != nil || p == nil { + http.Error(w, "invalid lifecycle payload", http.StatusBadRequest) + return } if action == "release" && p["reason"] == nil && p["handoff_ref"] == nil { - p["reason"] = "released_by_api" + http.Error(w, "reason or handoff_ref required", http.StatusBadRequest) + return } if action == "block" && p["blocker"] == nil { - p["blocker"] = "blocked_by_api" + http.Error(w, "blocker required", http.StatusBadRequest) + return } if action == "complete" && p["report_ref"] == nil { - ref, putErr := s.PutArtifact([]byte("completed without an attached report\n")) - if putErr != nil { - http.Error(w, putErr.Error(), 500) - return - } - p["report_ref"] = ref + http.Error(w, "report_ref required", http.StatusBadRequest) + return } ePayload, _ := json.Marshal(p) e = domain.Event{ID: id(), Type: types[action], TaskID: taskID, Version: t.Version + 1, Payload: ePayload} diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 3e6e777..4ddaf6c 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -72,3 +72,23 @@ func TestArtifactIsContentAddressed(t *testing.T) { t.Fatal(err) } } + +func TestLifecycleEventsRequireEvidence(t *testing.T) { + cases := []struct { + name string + typ string + body string + }{ + {"release", "TaskReleased", `{}`}, + {"complete", "TaskCompleted", `{}`}, + {"block", "TaskBlocked", `{}`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := domain.ValidateEvent(domain.Event{Type: tc.typ, TaskID: "task-1", Version: 1, Payload: json.RawMessage(tc.body)}) + if err == nil { + t.Fatal("expected lifecycle evidence validation error") + } + }) + } +} diff --git a/progress.md b/progress.md index c74a45e..f3b7391 100644 --- a/progress.md +++ b/progress.md @@ -51,6 +51,8 @@ The first pass closed the store/API defects (lifecycle defaults, CAS content ver The latest pass now applies `AuthorizeEvent` to lifecycle and approval writes and adds `/readyz` with router/provider configuration checks. Readiness is configuration-level only; it does not yet probe herdr/provider health. +The lifecycle API contract pass now requires callers to provide explicit release evidence (`reason` or `handoff_ref`), a block `blocker`, and a completion `report_ref`. The server no longer creates generated placeholder completion artifacts or converts malformed/empty lifecycle bodies into defaults. Regression coverage validates that release, completion, and block events reject missing evidence. + Recommended order: 1. Add the orchestration coordinator: lease → worktree → harness session → bootstrap → lifecycle events.