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
+27
View File
@@ -125,6 +125,13 @@ func phaseRequestBrief(phase domain.WorkPhase) string {
b.WriteString("\nAsk for one step. A request the project's path does not allow is refused, and the refusal names the phase you may ask for.\n")
if artifact := phaseSealFile[phase]; artifact != "" {
fmt.Fprintf(&b, "\nSeal .orchestra/%s before you ask. The request is refused without it.\n", artifact)
// The shape, not just the filename. Without it the agent has to guess
// a strict JSON schema from prose, and run 5 guessed dead_ends as
// strings where the decoder wants objects (F38). The request was then
// refused on every boundary for a field nobody had described.
if schema := phaseSealSchema[phase]; schema != "" {
fmt.Fprintf(&b, "\nIt must decode as this shape. Optional keys may be omitted, but no key may hold a different type:\n\n%s\n", schema)
}
}
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")
return b.String()
@@ -138,6 +145,26 @@ var phaseSealFile = map[domain.WorkPhase]string{
domain.WorkPhasePlan: "plan.json",
}
// phaseSealSchema is the shape of each sealed artifact, written out for the
// agent. TestPhaseSealSchemasDecode keeps these honest: each one is decoded by
// the same function the worker uses, so a struct change that is not mirrored
// here fails the build rather than a live run.
var phaseSealSchema = map[domain.WorkPhase]string{
domain.WorkPhaseResearch: ` {
"findings": [{"id": "", "claim": "", "evidence": "", "confidence": "fact|inference|assumption"}],
"relevant_code": [{"path": "", "why": ""}],
"invariants": [""],
"dead_ends": [{"tried": "", "why_failed": ""}],
"unknowns": [""]
}`,
domain.WorkPhasePlan: ` {
"changes": [{"target": "", "intent": ""}],
"verification": [""],
"risks": [""],
"human_decisions_needed": [""]
}`,
}
// askingBrief narrows step 4 per phase. The bar is not the same everywhere: a
// research phase that has not looked yet has no standing to ask, and an
// implementation phase asks only when a discovery invalidates the trajectory