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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user