Give the phase brief a protocol, and end the session it advances

The brief told the agent to ask for a phase change and never carried the
asking. The agent asked in prose, no code represented the request, and the
session idled until its lease expired. That is what failed run 3.

F21. The agent asks with .orchestra/phase-request.json, and seals
research.json or plan.json where the phase it is leaving produces one. At a
verified turn boundary the worker checks the phase belief, the transition and
the artifact, then calls the coordinator with its lease epoch and a derived
operation id. AdvanceWorkPhase is unchanged, so a request cannot reach a move
the operator surface could not also make. Redelivery is idempotent.

F22. A session now records the phase it was launched to run. One that no
longer matches its task rotates with reason phase_changed, whether this worker
asked for the change or an operator made it.

F20. CLIAdapter.prompt sent handoff and rotation prompts without confirming
them, which is the failure F20 exists to catch. Fixed at the shared call site.

F23 needed no change. Issue comments already become decisions with no
submission, through Reconciler.Reconcile at PreLease and at every turn
boundary. The earlier finding searched internal/operations alone and was
wrong. Tests now cover the boundary it turns on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
2026-08-27 16:35:37 +04:00
parent 15408a5463
commit 1f5bf7e66e
14 changed files with 1380 additions and 2 deletions
+21 -2
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"orchestra/internal/continuity"
"os"
"os/exec"
@@ -173,7 +174,23 @@ func (a CLIAdapter) prompt(ctx context.Context, s Session, text string, wait tim
if client, ok := backend.(*Client); ok {
client.BindAgent(s.PaneID, s.AgentName)
}
return backend.Prompt(ctx, s.PaneID, text, wait)
if err := backend.Prompt(ctx, s.PaneID, text, wait); err != nil {
return err
}
// Every Orchestra-originated pane write confirms (F20). This is the
// shared path for handoff and rotation prompts, so leaving it unconfirmed
// left the exact failure F20 exists to catch: a prompt sitting unsubmitted
// in the editor while Orchestra waits for a reply that cannot come.
c, ok := backend.(InputConfirmer)
if !ok {
return nil
}
evidence, err := c.ConfirmInput(ctx, s, text)
if err != nil {
return err
}
log.Printf("input to %s confirmed: %s", s.PaneID, evidence)
return nil
}
const handoffPrompt = `Orchestra is about to rotate this task. Write ONLY the following labelled answers to ` + HandoffReportFile + `, then stop. Output nothing else.
@@ -216,6 +233,8 @@ func (a CLIAdapter) RequestHandoffReason(ctx context.Context, s Session, reason
sb.WriteString("Signs of thrashing were detected (repeated failing test runs, repeated edits to the same file, or the same tool call repeated back to back). Stop the current approach rather than trying it again.\n")
case "milestone":
sb.WriteString("A coherent unit of work looks complete (a successful commit). If the next step is independent of what you just did, this is a good point to hand off.\n")
case "phase_changed":
sb.WriteString("This task has moved to its next work phase, so this session's context is no longer the right one for it. This is not a judgement about your work: the next phase starts fresh with the result you sealed. Stop at a clean point and hand off.\n")
case "reconcile_failure":
sb.WriteString("Orchestra cannot currently read the human input for this task, so it can no longer guarantee your instructions are current. Stop at a clean point and hand off. This is not a judgement about your work.\n")
}
@@ -528,7 +547,7 @@ func (a CLIAdapter) lastObservedCommand(s Session) string {
func handoffReason(s Session) string {
switch s.HandoffReason {
case "threshold", "milestone", "thrash", "manual", "reconcile_failure":
case "threshold", "milestone", "thrash", "manual", "reconcile_failure", "phase_changed":
return s.HandoffReason
default:
return "threshold"
+5
View File
@@ -160,6 +160,11 @@ type Session struct {
// session's lease was created — the immutable-spec hash continuity's
// pickup validation compares against on the next rotation (§6.2).
TaskFileSHA string `json:"task_file_sha,omitempty"`
// Phase is the work phase this session was launched in. Orchestra may
// advance the phase while the session runs; a session that no longer
// matches its task's phase is finished, because a phase change is a
// change of cognitive context and not a change of instruction.
Phase string `json:"phase,omitempty"`
// DeliveredDecisions holds the ids of the human decisions this session has
// already been shown. A decision recorded while the lease is live is
// delivered at the next verified turn boundary, and recording it here is