Tell the terminal phase how to finish
The worker finalises a task when .orchestra/done appears. No brief ever named that file: grepping a rendered launch.md for it returned nothing, in any phase. review is terminal. Its only legal move is backwards to implement, so a review that passes has nothing to ask for and, until now, nothing to write either. Run 5 halted exactly there after four clean rotations, with no error anywhere, because stopping was the correct reading of its instructions. The brief now names the marker in the terminal phase, says when to write it, and says it is exclusive with asking to go back. The test asserts the negative too, so a phase that can still ask is never told to finish instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
@@ -1695,3 +1695,24 @@ below all of them. That is the target chain, live.
|
||||
|
||||
Run 5 is implementing, not finished. The chain this run set out to prove is
|
||||
proven. Completion, the review phase, and the final report are still ahead.
|
||||
|
||||
### F40: the terminal phase never learned how to finish
|
||||
|
||||
Run 5's review agent finished at 02:12 and stopped. No `phase-request.json`, no
|
||||
handoff, no `.orchestra/done`, and nothing in the worker journal after 02:06:21.
|
||||
|
||||
`legalPhaseTransitions` makes review terminal: its only move is backwards to
|
||||
`implement`, for rework. A review that passes has nothing to ask for.
|
||||
|
||||
The worker has always finalised on `.orchestra/done`
|
||||
(`cmd/orchestra-worker/main.go:546`). No brief ever named that file. Grepping a
|
||||
rendered `launch.md` for it returned zero matches, in every phase.
|
||||
|
||||
So the completion signal existed on one side of the contract only. An agent
|
||||
whose review passed had no instruction at all, and stopping was the correct
|
||||
reading of what it had been told.
|
||||
|
||||
Fixed in the phase brief: the terminal phase now states the marker, when to
|
||||
write it, and that it is exclusive with asking to go back.
|
||||
`TestTerminalPhaseNamesTheCompletionSignal` also asserts the negative, so a
|
||||
phase that can still ask is never told to finish instead.
|
||||
|
||||
@@ -134,9 +134,23 @@ func phaseRequestBrief(phase domain.WorkPhase) string {
|
||||
}
|
||||
}
|
||||
b.WriteString("\nAn accepted request ends this session and starts the next phase with your sealed result. Saying you are ready in the pane is not a request and nothing reads it.\n")
|
||||
// How to finish. review's only transition is backwards to implement, so a
|
||||
// review that passes has nowhere to ask for. The worker has always watched
|
||||
// for .orchestra/done, but no brief ever named it (F40): grep for it in a
|
||||
// rendered launch.md returned nothing. A review agent that found no
|
||||
// problems therefore had no instruction at all and simply stopped, which
|
||||
// is where run 5 halted after four clean rotations.
|
||||
if completionPhase[phase] {
|
||||
b.WriteString("\nWhen the work satisfies the goal, the decisions and the accepted plan, finish the task: write .orchestra/done at the end of a turn, as your last act. Orchestra confirms you are idle before it finalises, so an empty file is the whole signal. If the work does not pass, ask to go back instead. Do not write both.\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// completionPhase is where finishing the task is the agent's to signal. It is
|
||||
// the phase with no forward move: asking is not available, so without this the
|
||||
// brief would offer only the way back.
|
||||
var completionPhase = map[domain.WorkPhase]bool{domain.WorkPhaseReview: true}
|
||||
|
||||
// phaseSealFile is the artifact a phase must seal before it may be left. It
|
||||
// mirrors the worker's table; both exist because the agent needs to be told
|
||||
// and the worker needs to check.
|
||||
|
||||
@@ -385,3 +385,26 @@ func TestPhaseSealSchemasDecode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTerminalPhaseNamesTheCompletionSignal guards F40. The worker finalises a
|
||||
// task when .orchestra/done appears, but no brief ever named that file. review
|
||||
// is terminal, its only legal move is backwards to implement, so a review that
|
||||
// passed had nothing to ask for and no way to finish. Run 5 halted there after
|
||||
// four clean rotations, silently.
|
||||
func TestTerminalPhaseNamesTheCompletionSignal(t *testing.T) {
|
||||
for phase := range completionPhase {
|
||||
if forward := domain.NextPhases(phase); len(forward) > 0 && forward[0] != domain.WorkPhaseImplement {
|
||||
t.Fatalf("%s is treated as terminal but moves forward to %s", phase, forward[0])
|
||||
}
|
||||
brief := phaseRequestBrief(phase)
|
||||
if !strings.Contains(brief, ".orchestra/done") {
|
||||
t.Fatalf("%s brief never names the completion signal:\n%s", phase, brief)
|
||||
}
|
||||
}
|
||||
// A phase that can still ask must not be told to finish instead.
|
||||
for _, phase := range []domain.WorkPhase{domain.WorkPhaseFrame, domain.WorkPhaseResearch, domain.WorkPhasePlan, domain.WorkPhaseImplement} {
|
||||
if strings.Contains(phaseRequestBrief(phase), ".orchestra/done") {
|
||||
t.Fatalf("%s can still ask for a phase change, so it must not be told to finish", phase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user