Give the manual plan-phase gate a producer

The gate had two live consumers and no producer. store.go resolves a pending
sign-off by subject, and manuallySignedOff checks for a prior one, but every
comment Orchestra imported was hardcoded to operator_instruction. A phase
carrying a manual step could reach awaiting_manual_verification and never
leave it.

A comment whose first line reads "orchestra verify <phase-id>" now approves
that phase. Orchestra supplies the plan ref from the task's own accepted plan,
so the approval binds to the plan that was current when the human wrote it.
Everything else still lands under operator_instruction, which is what keeps a
generic "looks good" from satisfying a gate nobody was discussing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 14:15:24 +04:00
parent 4b320809bd
commit 49409c9dd3
2 changed files with 58 additions and 1 deletions
+33
View File
@@ -288,3 +288,36 @@ func TestReconcileUsesOnlyTheTaskOwnSource(t *testing.T) {
t.Fatalf("an unowned task reached a source: owner %d, other %d", owner.calls, other.calls)
}
}
// A manual plan-phase gate is satisfied by a keyed comment and by nothing
// else. Without a producer for this subject the gate was unreachable: every
// imported comment landed under operator_instruction, so a phase with a
// manual step could never become verified.
func TestOnlyAKeyedCommentApprovesAPlanPhase(t *testing.T) {
task := domain.Task{ID: "t1", PlanRef: "plan-ref-a"}
want := domain.PlanPhaseSubject("plan-ref-a", "phase-2")
for _, c := range []struct {
body string
subject string
}{
{"looks good to me", operatorInstructionSubject},
{"orchestra verify phase-2", want},
{"Orchestra Verify phase-2", want},
{"orchestra verify phase-2\n\nthe json line read right", want},
{"orchestra verify ", operatorInstructionSubject},
{"please orchestra verify phase-2", operatorInstructionSubject},
} {
if got := decisionSubject(task, c.body); got != c.subject {
t.Errorf("decisionSubject(%q) = %q, want %q", c.body, got, c.subject)
}
}
// No accepted plan means no phase to approve.
if got := decisionSubject(domain.Task{ID: "t1"}, "orchestra verify phase-2"); got != operatorInstructionSubject {
t.Errorf("without a plan ref: got %q", got)
}
// The subject binds the plan, so the same words under a later plan do not
// satisfy a gate earned under the earlier one.
if decisionSubject(domain.Task{ID: "t1", PlanRef: "plan-ref-b"}, "orchestra verify phase-2") == want {
t.Error("a different plan ref produced the same subject")
}
}