Skip to content

devcontract.synthesize_result: a present-but-blank status: blocks the post-kill rescue #369

Description

@pbean

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:engineOrchestrator engine and run lifecyclebugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions