Surfaced while validating the reviewer findings on #355. Latent — no reported failure — but the code and the comment next to it disagree, so one of them is wrong.
The disagreement
devcontract.RECONCILABLE_FROM is the allowlist of frontmatter statuses a half-finalized generic spec may be reconciled from when its prose ## Auto Run Result says done. Its comment says:
"" covers a blank or missing frontmatter status: — reset_spec_status fills/inserts the line in that case.
A missing status: does read as "". A blank one does not. YAML parses status: with no value as None, and frontmatter.status_of normalizes with str(fm.get("status", "")), so None becomes the string "none":
>>> fm = read_frontmatter(spec) # spec contains a bare `status:` line
{'status': None, 'baseline_revision': 'base'}
>>> status_of(fm)
'none'
>>> status_of(fm) in devcontract.RECONCILABLE_FROM
False
So a spec with a blank status: — which the comment says is covered — falls into the "unknown custom token" arm and is left untouched, and the reconcile the comment promises never runs.
Why it matters
"none" is not a token anything in the project writes, so nothing distinguishes it from a genuine unknown custom status a skill set on purpose. The allowlist's whole design point is "never override a status the skill set deliberately" — and here it declines to reconcile a spec that carries no deliberate status at all.
A bmad-dev-auto template can leave the status blank; devcontract._FM_STATUS_RE says so in its own comment ("the value is * (not +) so a present-but-empty status (status: / status: "") is matched and filled — a bmad-dev-auto template can leave it blank"). So the writer explicitly supports the shape the reader silently excludes.
Fix shape (needs a decision)
Either normalize None to "" in status_of — which is the reading every gate goes through, so it touches more than this allowlist and needs its own characterization — or add "none" to RECONCILABLE_FROM, which is narrower but leaves a stringified None as a load-bearing token, which is worse. My read is the first, but the blast radius wants measuring before anyone writes it.
Note status_of's docstring calls itself "the single point all spec-frontmatter status gates read through", so a change there is deliberately wide by design.
Surfaced while validating the reviewer findings on #355. Latent — no reported failure — but the code and the comment next to it disagree, so one of them is wrong.
The disagreement
devcontract.RECONCILABLE_FROMis the allowlist of frontmatter statuses a half-finalized generic spec may be reconciled from when its prose## Auto Run Resultsaysdone. Its comment says:A missing
status:does read as"". A blank one does not. YAML parsesstatus:with no value asNone, andfrontmatter.status_ofnormalizes withstr(fm.get("status", "")), soNonebecomes the string"none":So a spec with a blank
status:— which the comment says is covered — falls into the "unknown custom token" arm and is left untouched, and the reconcile the comment promises never runs.Why it matters
"none"is not a token anything in the project writes, so nothing distinguishes it from a genuine unknown custom status a skill set on purpose. The allowlist's whole design point is "never override a status the skill set deliberately" — and here it declines to reconcile a spec that carries no deliberate status at all.A bmad-dev-auto template can leave the status blank;
devcontract._FM_STATUS_REsays so in its own comment ("the value is*(not+) so a present-but-empty status (status:/status: "") is matched and filled — a bmad-dev-auto template can leave it blank"). So the writer explicitly supports the shape the reader silently excludes.Fix shape (needs a decision)
Either normalize
Noneto""instatus_of— which is the reading every gate goes through, so it touches more than this allowlist and needs its own characterization — or add"none"toRECONCILABLE_FROM, which is narrower but leaves a stringifiedNoneas a load-bearing token, which is worse. My read is the first, but the blast radius wants measuring before anyone writes it.Note
status_of's docstring calls itself "the single point all spec-frontmatter status gates read through", so a change there is deliberately wide by design.