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:
@@ -30,8 +30,8 @@ func (a *fakeAdapter) Release(context.Context, herdr.Session) (string, error) {
|
||||
a.releases++
|
||||
return a.ref, nil
|
||||
}
|
||||
func (a *fakeAdapter) Kill(context.Context, herdr.Session) error { return nil }
|
||||
func (a *fakeAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil }
|
||||
func (a *fakeAdapter) Kill(context.Context, herdr.Session) error { return nil }
|
||||
func (a *fakeAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil }
|
||||
func (a *fakeAdapter) AtTurnBoundary(context.Context, herdr.Session) (bool, error) {
|
||||
return a.boundary, nil
|
||||
}
|
||||
@@ -137,6 +137,108 @@ func TestRotationEmitsValidReleaseWithAnchorSHA(t *testing.T) {
|
||||
|
||||
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
|
||||
|
||||
// TestTurnDecision guards AUDIT.md Phase 2 items 1-2: the synchronous,
|
||||
// per-turn counterpart to rotate() must return the same verdicts the
|
||||
// periodic ticker would compute, and rotate_now must actually perform the
|
||||
// release (not just report what rotate() would eventually do).
|
||||
func TestTurnDecision(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
run(t, repo, "init")
|
||||
run(t, repo, "config", "user.email", "t@t")
|
||||
run(t, repo, "config", "user.name", "t")
|
||||
run(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
|
||||
newCoordinator := func(a *fakeAdapter) (*orchestrator.Coordinator, *store.Store, domain.Task) {
|
||||
s, err := store.Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{
|
||||
"source": "jsonl", "external_id": "1", "project": "p",
|
||||
})}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := s.Tasks()[0]
|
||||
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json", Hard: .8}
|
||||
leaseEvt, err := s.Lease(task.ID, "h1", time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Start(context.Background(), leaseEvt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c, s, task
|
||||
}
|
||||
|
||||
t.Run("continue below threshold", func(t *testing.T) {
|
||||
a := &fakeAdapter{occupancy: .5}
|
||||
c, _, task := newCoordinator(a)
|
||||
decision, err := c.TurnDecision(context.Background(), task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decision != orchestrator.TurnContinue {
|
||||
t.Fatalf("decision=%q want %q", decision, orchestrator.TurnContinue)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refuse when not at turn boundary", func(t *testing.T) {
|
||||
a := &fakeAdapter{occupancy: .95, boundary: false}
|
||||
c, _, task := newCoordinator(a)
|
||||
decision, err := c.TurnDecision(context.Background(), task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decision != orchestrator.TurnRefuse {
|
||||
t.Fatalf("decision=%q want %q", decision, orchestrator.TurnRefuse)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rotate_now releases and emits a valid TaskReleased", func(t *testing.T) {
|
||||
a := &fakeAdapter{occupancy: .95, boundary: true}
|
||||
c, st, task := newCoordinator(a)
|
||||
artifactRef, err := st.PutArtifact([]byte("handoff"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.ref = artifactRef
|
||||
decision, err := c.TurnDecision(context.Background(), task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decision != orchestrator.TurnRotateNow {
|
||||
t.Fatalf("decision=%q want %q releases=%d", decision, orchestrator.TurnRotateNow, a.releases)
|
||||
}
|
||||
if a.releases == 0 {
|
||||
t.Fatal("adapter Release was never invoked")
|
||||
}
|
||||
got, ok := st.Task(task.ID)
|
||||
if !ok || got.State != domain.StateQueued {
|
||||
t.Fatalf("task state=%v ok=%v, want queued", got.State, ok)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prepare_handoff requests handoff without releasing", func(t *testing.T) {
|
||||
a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .95, boundary: true}}
|
||||
c, st, task := newCoordinator(&a.fakeAdapter)
|
||||
c.Adapters = adapters{a}
|
||||
decision, err := c.TurnDecision(context.Background(), task.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decision != orchestrator.TurnPrepareHandoff {
|
||||
t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff)
|
||||
}
|
||||
if a.releases != 0 {
|
||||
t.Fatal("adapter Release was invoked, expected only a handoff request")
|
||||
}
|
||||
got, ok := st.Task(task.ID)
|
||||
if !ok || got.State != domain.StateLeased {
|
||||
t.Fatalf("task state=%v ok=%v, want leased", got.State, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestStartBlocksOnInvalidPickup guards AUDIT.md's B6/Phase 4 item 4:
|
||||
// Coordinator.Start must run §6.2 pickup validation against the real
|
||||
// worktree before bootstrapping a successor onto a handoff_ref, and refuse
|
||||
@@ -294,8 +396,8 @@ func (a *noBoundaryAdapter) Release(context.Context, herdr.Session) (string, err
|
||||
a.releases++
|
||||
return a.ref, nil
|
||||
}
|
||||
func (a *noBoundaryAdapter) Kill(context.Context, herdr.Session) error { return nil }
|
||||
func (a *noBoundaryAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil }
|
||||
func (a *noBoundaryAdapter) Kill(context.Context, herdr.Session) error { return nil }
|
||||
func (a *noBoundaryAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil }
|
||||
|
||||
func setupRotationTask(t *testing.T, repo string) (*store.Store, string, domain.Task, string) {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user