Summary
STORY_RE in src/bmad_loop/sprintstatus.py cannot parse a story key whose story number carries a letter suffix — the shape BMAD produces whenever an oversized story is split into 2.6a / 2.6b.
Such keys are classified as unknown and the stories are silently skipped: they never appear in the backlog count, --story cannot select them, and bmad-loop run walks straight past them to the next numeric story. The only trace is a sprint-status-unknown-keys line in the run journal — nothing surfaces on the CLI.
Observed on v0.8.1 (current latest); the regexes are unchanged on the default branch.
Root cause
https://github.com/bmad-code-org/bmad-loop/blob/main/src/bmad_loop/sprintstatus.py#L21-L22
STORY_RE = re.compile(r"^(\d+)-(\d+)-(.+)$")
SHORT_REF_RE = re.compile(r"^(\d+)[-.](\d+)$") # short story ref: 3-1 or 3.1
Against 2-6a-example-slug: ^(\d+)- consumes 2-, then (\d+)- needs digits-then-hyphen but finds 6a → no match. STORY_RE fails, so the key falls through to the unknown bucket at L107 and the story is dropped from stories.
SHORT_REF_RE has the same flaw, which is why --story 2-6a also reports no story matches '2-6a'.
Reproduction
# sprint-status.yaml
2-5-example-slug: done
2-6a-example-slug: backlog # <-- invisible to the loop
2-6b-example-slug: backlog # <-- invisible to the loop
2-7-example-slug: backlog
$ bmad-loop run --epic 2 --max-stories 1
# picks 2-7, silently skipping 2-6a and 2-6b
$ bmad-loop run --story 2-6a
no story matches '2-6a'
Impact
Any story split with a letter suffix becomes invisible to the orchestrator. Since splitting is what you do to an oversized story, this drops precisely the stories that were deliberately made tractable for the loop.
The failure mode is the dangerous kind: it is silent and it reorders work. A split pair like N.a (build a structure) / N.b (extend it) gets skipped, and the loop instead starts a later story that depends on N.a having been built — an out-of-order build that is wasted effort. Nothing in the CLI output indicates a story was skipped.
Proposed fix
Match the suffix but do not capture it, so group(2) stays digits-only. This matters: num=int(m.group(2)) (L113) and int(m.group(2)) (L284, L288) would raise ValueError: invalid literal for int() with base 10: '6a' if the suffix were captured with a naive (\d+[a-z]?). Not capturing also preserves the existing group numbering, so group(3) remains the slug.
STORY_RE = re.compile(r"^(\d+)-(\d+)[a-z]?-(.+)$")
SHORT_REF_RE = re.compile(r"^(\d+)[-.](\d+)[a-z]?$")
Story.key already carries the full unique key and nothing reconstructs a key from epic+num, so split stories stay distinct and correctly ordered (file order breaks the 6a/6b tie).
Verified:
| key |
current |
fixed |
2-5-example-slug |
MATCH |
MATCH |
2-6a-example-slug |
SKIP |
MATCH |
4-3c-example-slug |
SKIP |
MATCH |
6-1b-example-slug |
SKIP |
MATCH |
Applying this locally recovered every previously-invisible split story, and the loop then selected 2-6a correctly instead of jumping ahead.
One caveat worth a design decision
With the suffix uncaptured, 2-6a and 2-6b both resolve to num=6, so a short ref (--story 2-6) matches both and selection falls to the first actionable. Full keys and slug fragments still select exactly. If you'd rather short refs disambiguate, the suffix needs its own field on Story / StorySelector — a larger change than the two-line fix above, which is why I've kept this report to the minimal correctness fix.
Suggestion beyond the fix
Consider surfacing unknown sprint-status keys on the CLI (a warning line, or a non-zero exit under a strict flag) rather than only in the run journal. Silently dropping a backlog story reads to the operator as "that story is done, or not mine to do," when in fact the orchestrator simply could not parse it.
Summary
STORY_REinsrc/bmad_loop/sprintstatus.pycannot parse a story key whose story number carries a letter suffix — the shape BMAD produces whenever an oversized story is split into2.6a/2.6b.Such keys are classified as unknown and the stories are silently skipped: they never appear in the backlog count,
--storycannot select them, andbmad-loop runwalks straight past them to the next numeric story. The only trace is asprint-status-unknown-keysline in the run journal — nothing surfaces on the CLI.Observed on
v0.8.1(current latest); the regexes are unchanged on the default branch.Root cause
https://github.com/bmad-code-org/bmad-loop/blob/main/src/bmad_loop/sprintstatus.py#L21-L22
Against
2-6a-example-slug:^(\d+)-consumes2-, then(\d+)-needs digits-then-hyphen but finds6a→ no match.STORY_REfails, so the key falls through to the unknown bucket at L107 and the story is dropped fromstories.SHORT_REF_REhas the same flaw, which is why--story 2-6aalso reportsno story matches '2-6a'.Reproduction
Impact
Any story split with a letter suffix becomes invisible to the orchestrator. Since splitting is what you do to an oversized story, this drops precisely the stories that were deliberately made tractable for the loop.
The failure mode is the dangerous kind: it is silent and it reorders work. A split pair like
N.a(build a structure) /N.b(extend it) gets skipped, and the loop instead starts a later story that depends onN.ahaving been built — an out-of-order build that is wasted effort. Nothing in the CLI output indicates a story was skipped.Proposed fix
Match the suffix but do not capture it, so
group(2)stays digits-only. This matters:num=int(m.group(2))(L113) andint(m.group(2))(L284, L288) would raiseValueError: invalid literal for int() with base 10: '6a'if the suffix were captured with a naive(\d+[a-z]?). Not capturing also preserves the existing group numbering, sogroup(3)remains the slug.Story.keyalready carries the full unique key and nothing reconstructs a key fromepic+num, so split stories stay distinct and correctly ordered (file order breaks the6a/6btie).Verified:
2-5-example-slug2-6a-example-slug4-3c-example-slug6-1b-example-slugApplying this locally recovered every previously-invisible split story, and the loop then selected
2-6acorrectly instead of jumping ahead.One caveat worth a design decision
With the suffix uncaptured,
2-6aand2-6bboth resolve tonum=6, so a short ref (--story 2-6) matches both and selection falls to the first actionable. Full keys and slug fragments still select exactly. If you'd rather short refs disambiguate, the suffix needs its own field onStory/StorySelector— a larger change than the two-line fix above, which is why I've kept this report to the minimal correctness fix.Suggestion beyond the fix
Consider surfacing unknown sprint-status keys on the CLI (a warning line, or a non-zero exit under a strict flag) rather than only in the run journal. Silently dropping a backlog story reads to the operator as "that story is done, or not mine to do," when in fact the orchestrator simply could not parse it.