Tell the agent the artifact shape, and tell it when the shape is wrong

Two halves of the same failure, live on run 5.

F38: the phase brief named .orchestra/research.json and described its contents
in prose, never its schema. The agent guessed dead_ends as strings where the
decoder wants {tried, why_failed} objects. The brief now carries the shape, and
a test decodes each documented shape with the same function the worker uses, so
a struct change that is not mirrored fails the build.

F39: the local artifact check refused the request through recordError alone.
answerRefusedPhase only ran on a coordinator 409, so a decode failure told the
agent nothing. The session sat at a boundary rewriting nothing, which is the
silent-loop shape the comment above that block warns about, reached by the one
path with no delivery. Both local refusals now reach the agent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
2026-08-28 01:58:36 +04:00
parent 87a3a6e2e4
commit 0bd86e28c6
3 changed files with 76 additions and 8 deletions
+15 -8
View File
@@ -1834,19 +1834,26 @@ func (w *worker) requestPhase(ctx context.Context, id string, s herdr.Session) b
artifact, err = os.ReadFile(filepath.Join(s.Worktree, ".orchestra", name))
if err != nil {
w.recordError(fmt.Errorf("phase request %s: work phase %q must seal .orchestra/%s first: %w", id, req.From, name, err))
w.answerRefusedPhase(ctx, id, s, path, fmt.Sprintf("work phase %q must seal .orchestra/%s first: %v", req.From, name, err))
return false
}
var decErr error
switch req.From {
case domain.WorkPhaseResearch:
if _, decErr := workphase.DecodeResearch(artifact); decErr != nil {
w.recordError(fmt.Errorf("phase request %s: research artifact: %w", id, decErr))
return false
}
_, decErr = workphase.DecodeResearch(artifact)
case domain.WorkPhasePlan:
if _, decErr := workphase.DecodePlan(artifact); decErr != nil {
w.recordError(fmt.Errorf("phase request %s: plan artifact: %w", id, decErr))
return false
}
_, decErr = workphase.DecodePlan(artifact)
}
if decErr != nil {
w.recordError(fmt.Errorf("phase request %s: %s artifact: %w", id, req.From, decErr))
// A local refusal is still a refusal, and the agent is the only
// party that can fix it. Recording it in worker health alone left
// a live session parked at a boundary forever with nothing telling
// it what was wrong (F39) — the silent-loop shape the comment
// above this block warns about, reached by the one path that had
// no delivery.
w.answerRefusedPhase(ctx, id, s, path, fmt.Sprintf(".orchestra/%s does not match the schema: %v", name, decErr))
return false
}
}
l := w.leases[id]