From b9ca365b9a97ff1732852c0b885bf6e3a949e92f Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 28 Aug 2026 01:38:58 +0400 Subject: [PATCH] Let the validator accept the reason the adapter produces handoffReason has emitted "phase_changed" since phase rotations landed. The continuity validator's reason list was never extended, so every phase rotation built a handoff it then refused as "invalid handoff meta". Live on run 5: with F31 clearing the parse ahead of it, the release reached this and stopped at phase "prepared" with "adapter: upload handoff: invalid handoff meta". The test asserts the property rather than the constant: every reason the adapter can produce must survive Validate, including its fallback. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu --- internal/continuity/continuity.go | 7 ++++++- internal/herdr/adapter_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/continuity/continuity.go b/internal/continuity/continuity.go index 446c5e2..24cbbe8 100644 --- a/internal/continuity/continuity.go +++ b/internal/continuity/continuity.go @@ -132,7 +132,12 @@ type Result struct { // reconcile_failure is Orchestra's own trigger: human input could not be // reconciled at repeated verified turn boundaries, so the session is handed to // a successor rather than left running on intent that cannot be refreshed. -var reasons = map[string]bool{"threshold": true, "milestone": true, "thrash": true, "manual": true, "reconcile_failure": true} +// phase_changed is a rotation Orchestra itself triggers when a phase request +// is accepted. The adapter has produced it since phase rotations landed +// (handoffReason), but this list was never extended, so every phase rotation +// built a handoff the validator then refused as "invalid handoff meta" (F36). +// The two halves of one contract have to name the same vocabulary. +var reasons = map[string]bool{"threshold": true, "milestone": true, "thrash": true, "manual": true, "reconcile_failure": true, "phase_changed": true} const maxAuthoredLine = 200 diff --git a/internal/herdr/adapter_test.go b/internal/herdr/adapter_test.go index e1b4cfd..d930ca2 100644 --- a/internal/herdr/adapter_test.go +++ b/internal/herdr/adapter_test.go @@ -315,3 +315,22 @@ func TestReleaseRequiresCAS(t *testing.T) { t.Fatal("expected error when CAS is nil") } } + +// TestEveryReasonTheAdapterProducesIsAcceptedByTheValidator guards F36. The +// adapter gained "phase_changed" when phase rotations landed and the +// validator's vocabulary did not, so every phase rotation built a handoff that +// was then refused as "invalid handoff meta". A live run reached exactly this +// point after F31 unblocked the parse ahead of it. +func TestEveryReasonTheAdapterProducesIsAcceptedByTheValidator(t *testing.T) { + for _, reason := range []string{"threshold", "milestone", "thrash", "manual", "reconcile_failure", "phase_changed", "something the adapter does not know"} { + produced := handoffReason(Session{HandoffReason: reason}) + h := continuity.Handoff{ + Meta: continuity.Meta{ID: "agent-1", Reason: produced}, + Anchor: continuity.Anchor{GitSHA: "0000000000000000000000000000000000000000", Branch: "main"}, + Action: "write the research note", + } + if err := h.Validate(); err != nil { + t.Fatalf("adapter produces reason %q for %q, validator refuses it: %v", produced, reason, err) + } + } +}