Found while designing #358. That issue fixes frontmatter.status_of; this one is the second copy of the same stringification, in devcontract.synthesize_result, which #358's fix does not reach:
fm_status = str(fm.get("status", "")).strip().lower() # devcontract.py:257
The failure
A spec whose frontmatter carries a present-but-blank status: (YAML null) with a prose ## Auto Run Result saying done:
fm_status becomes "none", which is truthy
- so
status = fm_status or arr.status (line 271) keeps "none" and never falls back to the prose done
- so
consistent = (arr.status == status) is "done" == "none" → False
Reproduced against main:
present-but-blank `status:` → status: none status_consistent: False
no frontmatter block at all → status: done status_consistent: True
Why it matters
status_consistent is the gate on the post-kill rescue. adapters/generic.py::_post_kill_reconcile (#61) rescues a session that finished its work but lost its final Stop event, and its docstring states the intent explicitly:
the synthesis must be self-consistent (status_consistent — "no active disagreement"; a blank frontmatter with prose done passes, exactly what a delivered Stop would have synthesized, and the engine's reconcile repairs the lag)
The blank-frontmatter case is exactly the one that does not pass — so the rescue refuses the template shape its docstring promises to rescue, and a session that finished real, tested work is discarded as stalled/timeout.
Note the two "blank" shapes diverge, which is why this went unnoticed: a spec with no frontmatter block synthesizes done and rescues fine, while a spec with a present-but-blank status: does not. devcontract._FM_STATUS_RE's own comment says the writer side supports the present-but-blank shape ("a bmad-dev-auto template can leave it blank").
Test gap
tests/test_generic_tmux.py::test_post_kill_reconcile_rescues_consistent_done_artifact uses _DONE_SPEC, which carries status: done — a spec whose frontmatter and prose already agree. Nothing covers the blank-frontmatter shape the docstring names. The _spec fixture in tests/test_devcontract.py writes status: '{status}' quoted, so passing status="" yields an empty string, not YAML-null — the fixture cannot express the failing shape as written.
Fix
Read through frontmatter.status_of (which normalizes YAML-null to "" after #358) instead of re-stringifying locally, so the two readers cannot drift again. Then cover the present-but-blank rescue at the _post_kill_reconcile layer, not just at synthesize_result.
Found while designing #358. That issue fixes
frontmatter.status_of; this one is the second copy of the same stringification, indevcontract.synthesize_result, which #358's fix does not reach:The failure
A spec whose frontmatter carries a present-but-blank
status:(YAML null) with a prose## Auto Run Resultsayingdone:fm_statusbecomes"none", which is truthystatus = fm_status or arr.status(line 271) keeps"none"and never falls back to the prosedoneconsistent = (arr.status == status)is"done" == "none"→ FalseReproduced against
main:Why it matters
status_consistentis the gate on the post-kill rescue.adapters/generic.py::_post_kill_reconcile(#61) rescues a session that finished its work but lost its final Stop event, and its docstring states the intent explicitly:The blank-frontmatter case is exactly the one that does not pass — so the rescue refuses the template shape its docstring promises to rescue, and a session that finished real, tested work is discarded as
stalled/timeout.Note the two "blank" shapes diverge, which is why this went unnoticed: a spec with no frontmatter block synthesizes
doneand rescues fine, while a spec with a present-but-blankstatus:does not.devcontract._FM_STATUS_RE's own comment says the writer side supports the present-but-blank shape ("a bmad-dev-auto template can leave it blank").Test gap
tests/test_generic_tmux.py::test_post_kill_reconcile_rescues_consistent_done_artifactuses_DONE_SPEC, which carriesstatus: done— a spec whose frontmatter and prose already agree. Nothing covers the blank-frontmatter shape the docstring names. The_specfixture intests/test_devcontract.pywritesstatus: '{status}'quoted, so passingstatus=""yields an empty string, not YAML-null — the fixture cannot express the failing shape as written.Fix
Read through
frontmatter.status_of(which normalizes YAML-null to""after #358) instead of re-stringifying locally, so the two readers cannot drift again. Then cover the present-but-blank rescue at the_post_kill_reconcilelayer, not just atsynthesize_result.