Skip to content

Sprint-status story keys with a letter suffix (e.g. 2-6a) are silently skipped by STORY_RE #144

Description

@kcjpro

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions