Stop refusing a handoff over words the arrow already says

98f1b2d fixed the arrow and left the other half. "tried" and "failed because"
restate the field names either side of a separator that already says what the
line means, and the parser required both.

Run 9 lost three leases to this. Every refused line carried exactly the data
the fields want:

    tried run line ["bash", "scripts/test_healthcheck.sh"] for phase-3
      -> refused, policy allows only bash -n on one file
    tried finding ids "F1".."F8" in research.json
      -> schema requires lowercase letters, digits, dash or underscore

Both prefixes are now optional. A line with no cause and effect is still
refused, because the split is the thing being validated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-28 15:38:34 +04:00
parent fb7135e1d9
commit fda78cf6e6
2 changed files with 33 additions and 17 deletions
+8 -7
View File
@@ -198,7 +198,7 @@ const handoffPrompt = `Orchestra is about to rotate this task. Write ONLY the fo
NEXT: the single next action (one line, at most 200 characters).
WHY: why that is next (one line, at most 200 characters).
REMAINING: outstanding items, one line each, at most 200 characters each. If none: NONE.
DEAD ENDS: approaches tried that failed — "tried X → failed because Y", one per line ("->" is also accepted). If none: NONE.
DEAD ENDS: approaches tried that failed — "X → why it failed", one per line. Either arrow, and the words "tried"/"failed because" are optional. If none: NONE.
OPEN Q: unresolved decisions, one line each. If none: NONE.
LEARNED: constraints discovered that are NOT in TASK.md, one line each. If none: NONE.
@@ -511,12 +511,13 @@ func parseHandoffAnswer(answer string) (handoffAnswer, error) {
if len(parts) != 2 {
return out, fmt.Errorf("DEAD ENDS: want 'tried X → failed because Y'")
}
left, right := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
if !strings.HasPrefix(left, "tried ") || !strings.HasPrefix(right, "failed because ") {
return out, fmt.Errorf("DEAD ENDS: want 'tried X → failed because Y'")
}
tried := strings.TrimSpace(strings.TrimPrefix(left, "tried "))
why := strings.TrimSpace(strings.TrimPrefix(right, "failed because "))
// Both prefixes are optional. "tried" and "failed because" restate the
// field names either side of an arrow that already says what the line
// means, and run 9 lost three leases to an agent that dropped them
// while writing exactly the right content. What must be there is a
// cause and an effect, which is what the split establishes.
tried := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(parts[0]), "tried "))
why := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(parts[1]), "failed because "))
if tried == "" || why == "" {
return out, fmt.Errorf("DEAD ENDS: want 'tried X → failed because Y'")
}