feat(orchestrator): wire turn-decision endpoint and QuotaReported producer

Closes Phase 2 items 1-2 (AUDIT.md): Coordinator.TurnDecision evaluates
occupancy/turn-boundary/handoff state synchronously per turn and returns
continue/prepare_handoff/rotate_now/refuse, exposed via POST
/v1/harness/turn. The Claude Stop hook now calls it on ordinary turn
boundaries instead of no-op'ing, and exits 2 on refuse.

Also closes B7's post-hoc producer: /v1/harness/complete now appends a
QuotaReported event from the completing lease's harness usage, so the
router's quota-availability filter and the brief's quota_consumed stop
evaluating against a permanent zero. Live per-harness push producers
(Claude statusline, Codex rollout tail) remain unbuilt — investigation
recorded in AUDIT.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 23:17:21 +04:00
parent 0ca78243b9
commit 972845bd98
5 changed files with 382 additions and 21 deletions
+50
View File
@@ -302,8 +302,58 @@ func main() {
log.Printf("route task: %v", routeErr)
}
}
// B7 (AUDIT.md): QuotaReported has no other producer, so router's
// 5h/weekly availability filter and the brief's quota_consumed are
// permanently zero without this. Fed by the same per-harness usage
// read as the receipt above (spec §7.2 — "same per-harness session
// state as §5.2.1"); harness_id comes from the lease this completion
// closes out, before it's released.
if t.Lease != nil && t.Lease.HarnessID != "" {
qp, _ := json.Marshal(map[string]any{
"harness_id": t.Lease.HarnessID,
"consumed": float64(usage.Numerator()),
})
qe := domain.Event{ID: id(), Type: "QuotaReported", TaskID: "system", Payload: qp, Surface: string(authz.System)}
if err := s.Append(qe); err != nil {
log.Printf("quota report: %v", err)
}
}
json.NewEncoder(w).Encode(e)
})
// /v1/harness/turn is the unified turn-decision endpoint (AUDIT.md Phase
// 2 items 1-2): the Face-B stop hook posts here on every ordinary turn
// boundary (report marker absent — /v1/harness/complete covers task
// completion separately) and gets back exactly one of continue /
// prepare_handoff / rotate_now / refuse, per spec §5.3. This replaces
// what would otherwise be separate ad-hoc marker-file conventions per
// decision.
mux.HandleFunc("/v1/harness/turn", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if harnessToken != "" && r.Header.Get("Authorization") != "Bearer "+harnessToken {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if coordinator == nil {
http.Error(w, "coordinator not configured", http.StatusServiceUnavailable)
return
}
var p struct {
TaskID string `json:"task_id"`
}
if json.NewDecoder(r.Body).Decode(&p) != nil || p.TaskID == "" {
http.Error(w, "task_id is required", http.StatusBadRequest)
return
}
decision, err := coordinator.TurnDecision(r.Context(), p.TaskID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(map[string]string{"decision": decision})
})
mux.HandleFunc("/v1/brief", func(w http.ResponseWriter, r *http.Request) {
to := time.Now().UTC()
from := to.Add(-12 * time.Hour)