Skip to content

v7 preview ticketing: tickets.py status does not show the epic dependencies declared in the initiative's tickets.toml #2964

Description

@deduktion

Version

Skill bmad-preview-ticketing, installed with npx skills add bmad-code-org/BMAD-METHOD from ref dev
dev commit bc5f6ac17806e6e0b64935537b0a44682eb93ec0 (2026-09-24)
Byte-identical to that commit SKILL.md, customize.toml, references/slice.md, references/board.md, scripts/tickets.py (checked with cmp against raw.githubusercontent.com)
Same files on main identical to main @ a76af7f44efc9feabd6e05793e0e87b848a1c6a0
Store repo store, no customization of the ticketing skill
Runtime uv 0.12.18, Python 3.13.7, macOS arm64

Summary

An initiative's tickets.toml declares the epic graph: [[epic]] after = [{ epic, needs }]. tickets.py status <initiative> does not report that graph anywhere. epics[].after sounds like it should, but it holds something else: the whole-epic gates from each epic file's frontmatter. The only place the declared graph surfaces is unpinned_after, and only after an epic has entries.

That is consistent with the docs. slice.md:33 says: "after on an epic file only for a whole-epic gate". board.md:18 says: "a row's gated_by is its epic file's after". So this is a gap in visibility, not a crash. But the two meanings of after are hard to tell apart, and the declared dependencies are invisible exactly when they matter most: between slicing and inception.

How I ran into it

In a planning run on a real brownfield initiative, slicing produced six epics. The initiative's tickets.toml declares seven epic-level dependencies with needs. tickets.py status on the initiative then showed epics[].after as [] and blocks as [] for every epic. That reads as "no dependencies between epics". Only reading tickets.py explained the output:

  • load_container reads after from the epic file's frontmatter (tickets.py:201).
  • The initiative's [[epic]].after is used only in unpinned_after (tickets.py:510–520), which skips an epic without entries (if mine and not pinned).

After all six inceptions, the entries pinned 27 cross-epic edges. epics[].after still showed [] for every epic.

Reproduction (no domain content)

Two epics. B declares that it needs something from A. Neither epic file has a whole-epic gate (after: []).

T=<path to skill>/scripts/tickets.py       # bc5f6ac
R=$(mktemp -d)/initiative-demo && mkdir -p $R/epic-a $R/epic-b
printf -- '---\ntype: initiative\ntitle: "Demo"\nparent: none\ncovers: []\nassignee: ""\nrisk: low\n---\n' > $R/initiative-demo.md
cat > $R/tickets.toml <<'E'
[[epic]]
id = 1
slug = "epic-a"
title = "A"
covers = []

[[epic]]
id = 2
slug = "epic-b"
title = "B"
covers = []
after = [{ epic = 1, needs = "the contract A publishes" }]
E
for s in a b; do printf -- '---\ntype: epic\ntitle: "%s"\nparent: initiative-demo\ncovers: []\nafter: []\nassignee: ""\nrisk: low\n---\n' $s > $R/epic-$s/epic-$s.md; done
st() { uv run $T --project-root <project> status $R | jq -c '{epics, unpinned_after, longest_remaining_chain}'; }

st                                             # 1: after slicing, no entries
printf '[[entry]]\nid = 1\ntype = "story"\ntitle = "A1"\ndescription = "x"\nverify = "x"\nafter = []\n' > $R/epic-a/tickets.toml
printf '[[entry]]\nid = 1\ntype = "story"\ntitle = "B1"\ndescription = "x"\nverify = "x"\nafter = ["1.1"]\n' > $R/epic-b/tickets.toml
st                                             # 2: incepted, edge pinned
printf '[[entry]]\nid = 1\ntype = "story"\ntitle = "B1"\ndescription = "x"\nverify = "x"\nafter = []\n' > $R/epic-b/tickets.toml
st                                             # 3: incepted, edge not pinned
State epics[] after / blocks unpinned_after longest_remaining_chain
1 · after slicing, no entries A []/[], B []/[] [] []
2 · incepted, B.1 after = ["1.1"] A []/[], B []/[] [] ["1.1", "2.1"]
3 · incepted, B.1 after = [] A []/[], B []/[] [{epic: epic-b, after: epic-a, needs: "the contract A publishes"}] ["1.1"]

The declared dependency and its needs show up only in state 3, as a gap to fix. In state 1, when the dependency is the whole plan, and in state 2, when it is satisfied, status does not mention it.

Why it matters

  • Between slicing and inception, the plan is exactly this graph. The initiative's tickets.toml holds the build order and after + needs, and no entries exist yet. SKILL.md routes "status of a ticket or tree" to board.md, and board.md answers it with tickets.py status. At that stage, that answer contains no dependency at all. A person can read the initiative's tickets.toml directly; an agent following board.md reports what status returns.
  • The field name misleads. epics[].after in the output and [[epic]].after in the input share a name but mean different things: a whole-epic gate versus a partial dependency with needs. Reading after: [] suggests "no dependency".
  • The declared graph is checked in one direction only, and only once entries exist. See the companion issue on undeclared or order-violating cross-epic after (v7 preview ticketing: a cross-epic after on an entry that the initiative does not declare, or that points to a later epic, passes tickets.py without a warning #2963 ). With the declared graph in the output, both directions could be compared in one place.

Suggestion

Any of these would do:

  1. Add the declared graph to status on an initiative, for example epics[].needs = [{ epic: <slug>, needs: "..." }] from [[epic]].after. Report it whether or not the epic has entries.
  2. Rename the output field that holds the epic file's gates to gated_by, matching the row field board.md:18 already uses for the same thing. after would then be free for the declared graph. This changes the output format, so it is best done while the skill is still a preview.
  3. Let longest_remaining_chain count an epic without entries as one node, so the critical path between epics shows before inception.

Context

Found in the same run as the companion issue. Both are about the edge across the epic boundary. Since #2932 and #2934, that edge is machine-readable at entry level, and that part works well.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions