You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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: []).
[{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".
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.
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.
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.
Version
bmad-preview-ticketing, installed withnpx skills add bmad-code-org/BMAD-METHODfrom refdevdevcommitbc5f6ac17806e6e0b64935537b0a44682eb93ec0(2026-09-24)SKILL.md,customize.toml,references/slice.md,references/board.md,scripts/tickets.py(checked withcmpagainst raw.githubusercontent.com)mainmain@a76af7f44efc9feabd6e05793e0e87b848a1c6a0Summary
An initiative's
tickets.tomldeclares the epic graph:[[epic]] after = [{ epic, needs }].tickets.py status <initiative>does not report that graph anywhere.epics[].aftersounds 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 isunpinned_after, and only after an epic has entries.That is consistent with the docs.
slice.md:33says: "afteron an epic file only for a whole-epic gate".board.md:18says: "a row'sgated_byis its epic file'safter". So this is a gap in visibility, not a crash. But the two meanings ofafterare 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.tomldeclares seven epic-level dependencies withneeds.tickets.py statuson the initiative then showedepics[].afteras[]andblocksas[]for every epic. That reads as "no dependencies between epics". Only readingtickets.pyexplained the output:load_containerreadsafterfrom the epic file's frontmatter (tickets.py:201).[[epic]].afteris used only inunpinned_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[].afterstill 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: []).epics[]after / blocksunpinned_afterlongest_remaining_chain[]/[], B[]/[][][]after = ["1.1"][]/[], B[]/[][]["1.1", "2.1"]after = [][]/[], B[]/[][{epic: epic-b, after: epic-a, needs: "the contract A publishes"}]["1.1"]The declared dependency and its
needsshow 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,statusdoes not mention it.Why it matters
tickets.tomlholds the build order andafter+needs, and no entries exist yet.SKILL.mdroutes "status of a ticket or tree" toboard.md, andboard.mdanswers it withtickets.py status. At that stage, that answer contains no dependency at all. A person can read the initiative'stickets.tomldirectly; an agent followingboard.mdreports whatstatusreturns.epics[].afterin the output and[[epic]].afterin the input share a name but mean different things: a whole-epic gate versus a partial dependency withneeds. Readingafter: []suggests "no dependency".after(v7 preview ticketing: a cross-epicafteron an entry that the initiative does not declare, or that points to a later epic, passestickets.pywithout a warning #2963 ). With the declared graph in the output, both directions could be compared in one place.Suggestion
Any of these would do:
statuson an initiative, for exampleepics[].needs = [{ epic: <slug>, needs: "..." }]from[[epic]].after. Report it whether or not the epic has entries.gated_by, matching the row fieldboard.md:18already uses for the same thing.afterwould then be free for the declared graph. This changes the output format, so it is best done while the skill is still a preview.longest_remaining_chaincount 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.