Forward-port the 0.9.1 hotfix to main as a sequenced program of sub-phases.
PR #406 merged into release/0.9.x on 2026-08-02 and shipped 57 commits (+~2,000 net
source lines across 18 files, +12,076 test lines) after ~20 adversarial bot-review rounds.
main is 274 commits ahead of the v0.9.0 tag the hotfix was cut from, so this is a
re-implementation, not a cherry-pick: worktree_flow.py owns provisioning,
resolve_review_layers/_CUSTOMIZE_OVERRIDES exist and must follow the resolved name,
worktree excludes are worktree-scoped, and pyright is a configured CI gate that has never run
on any of this code.
bmad-build-auto has zero occurrences on main today — nothing pre-landed.
Sub-phases
Fifteen sessions, fourteen PRs. Every sub-phase leaves main green and coherent.
CONTRIBUTING's one-concern-per-PR rule was waived for the hotfix only; it applies here.
| # |
Sub-phase |
PR |
Closes |
| 6A |
resolution seam (+ tracking issue) |
1 |
— |
| 6B |
prompt threading + dry-run banner |
1 |
#393 |
| 6C |
small independent contracts + pyright canary |
2 |
— |
| 6D |
verify pathspec hardening |
3 |
#423 |
| 6E |
#414 refusal |
4 |
supersedes #414 |
| 6F |
renderer preflight |
5 |
resolves #410 |
| 6G |
provisioning totality |
6 |
#421, #422 |
| 6H |
_bmad seeding + escalations |
7 |
— |
| 6I |
deferral parser + ledger primitives + model fields |
8 |
— |
| 6J |
harvest + proof-of-work exclusion |
9 |
— |
| 6K |
snapshot / restore / disarm + ledger classification |
10 |
#420 |
| 6L |
accept-gated bundle close + reopen |
11 |
— |
| 6M |
isolation carry + crash replay |
12 |
— |
| 6N |
#425 + #426 + residuals |
13 |
#425, #426 |
| 6O |
CHANGELOG + program close-out |
14 |
— |
6C, 6D, 6E are order-independent among themselves. 6F→6G→6H is a chain; 6I→6J→6K→6L→6M is a
chain; the two chains are independent after 6E. Ship-worthy release points: after PR 1
(the fix users are waiting for), after PR 7, after PR 12.
Main-adaptation decisions (settled — do not re-litigate per session)
append_entry: port ZERO writer code. Main already emits location. Adopt main's
spelling and rewrite the 0.9.x tests that pin 0.9.x's field order. Re-landing
location: str | None = None silently reorders the field for every existing producer. The
harvest caller passes location=<value> or "n/a" — main's _one_line("") emits a blank
line, whereas the hotfix normalized empty → n/a inside the writer. The
deferred-work-format.md paragraph asserts source_spec: sits "directly after
origin:"; on main it sits after location: — port the paragraph with the ordering
claim corrected.
_CUSTOMIZE_OVERRIDES becomes _customize_overrides(resolved_name), reading the
RESOLVED name only. Keep skills.customize-legacy as the warning when a legacy file
exists with no new counterpart. ⚠️ A design pass recommended dual-reading (legacy pair
then new pair, later-wins) on the grounds that on main these files actually drive
_merged_review_layers, so warning alone drops the operator's layers. Rejected:
upstream's own resolver keys on the skill dir, so at run time the legacy file is
ignored — dual-reading would make the preflight resolve layers the session never applies,
which is exactly the failure resolve_review_layers's docstring exists to prevent
("Matching that keeps the preflight agreeing with the run"). The operator's layers really
are inert; the honest response is a warning that says so and names the rename. Word the
message accordingly, and keep it a warning, not a problem — see the false-green rule
below.
- Do NOT port
d1e60af's shared-exclude gating, and re-derive the /_bmad/render/
shield — see 6H's D-1.
- Merge kwargs, never replace:
verify_dev/verify_dev_bundle/verify_dev_stories/
verify_review take main's operator_park and the new engine_written. Make
engine_written keyword-only on all three, fixing 0.9.x's asymmetry (it is positional
on verify_dev_stories). ⚠️ On the stories plan-halt leg main passes
extra_exclude=None, which means skip the gate — leave it None; None + tuple is a
TypeError, and "fixing" it would arm a gate on a leg with no diff to satisfy it.
policy.dev.skill stays the literal "bmad-dev-auto" and DEV_SKILLS stays a
one-element set — it is the adapter discriminator read by engine.py:1996 and
runsetup.py:83. Improve the PolicyError text to say the invoked name is disk-resolved.
runsetup.py:83 and engine.py:1996 stay byte-identical.
- Extend
_copy_traversable; do not add a second copier (correction 3).
DEV_BASE_SKILLS stays keyed on LEGACY; NEW goes into BASE_SKILLS only (the
copy-if-present superset), leaving main's _REVIEW_LAYER_SKILLS/MERGED_REVIEW_SKILL
subsumption logic untouched.
Hard ordering constraints (violating any ships a silent bug)
install.DEV_PRIMITIVE_ROLES, cli._skill_trees and WorktreeFlow.worktree_profiles move
together. Main is already misaligned 3-vs-2 (correction 2); a one-sided move deepens it
in one direction — gate a tree no worktree carries (refuses runs over a skill nothing reads)
or provision a tree nothing gates (ships a session into the Unknown command stall the
preflight exists to catch). (6A)
RENDERER_SEED_SENTINELS crosses the install↔engine boundary by exact string
membership. The tuple may ship inert in 6F — that is fine. What is not fine is
shipping 6H's skipped producer without the renderer_stub_resolved conjunct: the
escalation then fires on every pre-#2601 inline-SKILL.md project. (6F → 6H)
HARVEST_ORIGIN = "spec-deferred" is the on-disk dedup key. Changing it re-files every
already-harvested finding. (6J)
verify.path_tracked lands before git.render-tracked (6F) and before
_ledger_is_gits_to_restore (6K) — hence 6D sits early. It ships in 6D with zero
callers; that is inert, not silent (neither ruff's E4,E7,E9,F set nor pyright basic
flags an unused module function). Justify it in the PR body and name its consumers.
- Vacuous on main —
append_entry(location=) already exists (decision 1).
model.py's 8 fields land before any reader, and each needs a line in both
to_dict:314 and from_dict:361 — they are hand-enumerated, so a field in only one is
silently lost across every save/load, and with no getattr fallbacks the reader sees the
default, not an error. (6I)
_post_dev_accepted_sync and the reduction of sweep._post_dev_state_sync to a no-op are
one change, or the bundle close runs twice at two different gate positions. (6L)
SweepEngine._carry_isolated_ledger_writes calls super() first (harvest before close
— reversed, the close lands in front of append_entry's open-only dedup and the harvest
files a duplicate id); isolated_ledger_carried is set at the call sites, never inside
the base hook (otherwise the resume replay cannot distinguish "carried" from "hook ran and
found nothing"). (6M)
Related
Forward-port the 0.9.1 hotfix to
mainas a sequenced program of sub-phases.PR #406 merged into
release/0.9.xon 2026-08-02 and shipped 57 commits (+~2,000netsource lines across 18 files,
+12,076test lines) after ~20 adversarial bot-review rounds.mainis 274 commits ahead of the v0.9.0 tag the hotfix was cut from, so this is are-implementation, not a cherry-pick:
worktree_flow.pyowns provisioning,resolve_review_layers/_CUSTOMIZE_OVERRIDESexist and must follow the resolved name,worktree excludes are worktree-scoped, and pyright is a configured CI gate that has never run
on any of this code.
bmad-build-autohas zero occurrences onmaintoday — nothing pre-landed.Sub-phases
Fifteen sessions, fourteen PRs. Every sub-phase leaves
maingreen and coherent.CONTRIBUTING's one-concern-per-PR rule was waived for the hotfix only; it applies here.
_bmadseeding + escalations6C, 6D, 6E are order-independent among themselves. 6F→6G→6H is a chain; 6I→6J→6K→6L→6M is a
chain; the two chains are independent after 6E. Ship-worthy release points: after PR 1
(the fix users are waiting for), after PR 7, after PR 12.
Main-adaptation decisions (settled — do not re-litigate per session)
append_entry: port ZERO writer code. Main already emitslocation. Adopt main'sspelling and rewrite the 0.9.x tests that pin 0.9.x's field order. Re-landing
location: str | None = Nonesilently reorders the field for every existing producer. Theharvest caller passes
location=<value> or "n/a"— main's_one_line("")emits a blankline, whereas the hotfix normalized empty →
n/ainside the writer. Thedeferred-work-format.mdparagraph assertssource_spec:sits "directly afterorigin:"; on main it sits afterlocation:— port the paragraph with the orderingclaim corrected.
_CUSTOMIZE_OVERRIDESbecomes_customize_overrides(resolved_name), reading theRESOLVED name only. Keep
skills.customize-legacyas the warning when a legacy fileexists with no new counterpart.
then new pair, later-wins) on the grounds that on main these files actually drive
_merged_review_layers, so warning alone drops the operator's layers. Rejected:upstream's own resolver keys on the skill dir, so at run time the legacy file is
ignored — dual-reading would make the preflight resolve layers the session never applies,
which is exactly the failure
resolve_review_layers's docstring exists to prevent("Matching that keeps the preflight agreeing with the run"). The operator's layers really
are inert; the honest response is a warning that says so and names the rename. Word the
message accordingly, and keep it a warning, not a problem — see the false-green rule
below.
d1e60af's shared-exclude gating, and re-derive the/_bmad/render/shield — see 6H's D-1.
verify_dev/verify_dev_bundle/verify_dev_stories/verify_reviewtake main'soperator_parkand the newengine_written. Makeengine_writtenkeyword-only on all three, fixing 0.9.x's asymmetry (it is positionalon
verify_dev_stories).extra_exclude=None, which means skip the gate — leave itNone;None + tupleis aTypeError, and "fixing" it would arm a gate on a leg with no diff to satisfy it.
policy.dev.skillstays the literal"bmad-dev-auto"andDEV_SKILLSstays aone-element set — it is the adapter discriminator read by
engine.py:1996andrunsetup.py:83. Improve thePolicyErrortext to say the invoked name is disk-resolved.runsetup.py:83andengine.py:1996stay byte-identical._copy_traversable; do not add a second copier (correction 3).DEV_BASE_SKILLSstays keyed on LEGACY; NEW goes intoBASE_SKILLSonly (thecopy-if-present superset), leaving main's
_REVIEW_LAYER_SKILLS/MERGED_REVIEW_SKILLsubsumption logic untouched.
Hard ordering constraints (violating any ships a silent bug)
install.DEV_PRIMITIVE_ROLES,cli._skill_treesandWorktreeFlow.worktree_profilesmovetogether. Main is already misaligned 3-vs-2 (correction 2); a one-sided move deepens it
in one direction — gate a tree no worktree carries (refuses runs over a skill nothing reads)
or provision a tree nothing gates (ships a session into the
Unknown commandstall thepreflight exists to catch). (6A)
RENDERER_SEED_SENTINELScrosses theinstall↔engineboundary by exact stringmembership. The tuple may ship inert in 6F — that is fine. What is not fine is
shipping 6H's
skippedproducer without therenderer_stub_resolvedconjunct: theescalation then fires on every pre-#2601 inline-
SKILL.mdproject. (6F → 6H)HARVEST_ORIGIN = "spec-deferred"is the on-disk dedup key. Changing it re-files everyalready-harvested finding. (6J)
verify.path_trackedlands beforegit.render-tracked(6F) and before_ledger_is_gits_to_restore(6K) — hence 6D sits early. It ships in 6D with zerocallers; that is inert, not silent (neither ruff's
E4,E7,E9,Fset nor pyright basicflags an unused module function). Justify it in the PR body and name its consumers.
append_entry(location=)already exists (decision 1).model.py's 8 fields land before any reader, and each needs a line in bothto_dict:314andfrom_dict:361— they are hand-enumerated, so a field in only one issilently lost across every save/load, and with no
getattrfallbacks the reader sees thedefault, not an error. (6I)
_post_dev_accepted_syncand the reduction ofsweep._post_dev_state_syncto a no-op areone change, or the bundle close runs twice at two different gate positions. (6L)
SweepEngine._carry_isolated_ledger_writescallssuper()first (harvest before close— reversed, the close lands in front of
append_entry's open-only dedup and the harvestfiles a duplicate id);
isolated_ledger_carriedis set at the call sites, never insidethe base hook (otherwise the resume replay cannot distinguish "carried" from "hook ran and
found nothing"). (6M)
Related
release/0.9.xvia 0.9.1: adopt upstream bmad-build-auto rename, harvest spec-frontmatter deferrals (#405) #406)## [0.9.1]CHANGELOG section is the best per-mechanism narrative(
test_skill_trees_covers_review_even_when_review_is_disabled) must survive