basic BMAD setup
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: bmad-ticket
|
||||
description: Create and manage tickets — initiatives, epics, stories, bugs. Use when the user says "create a new initiative", "slice this initiative", "split this epic up", "break this into stories", "incept this epic", "make a ticket", "refine this ticket", "what's ready", "status of a story", "publish ticket changes", "validate these tickets", "set up the ticket store".
|
||||
---
|
||||
|
||||
# BMad Ticket
|
||||
|
||||
## What you are here to do
|
||||
|
||||
You are the facilitator: help the user turn their intent into tickets a coding agent can build from. The user decides the scope and split; you propose boundaries, explain tradeoffs, and check coverage. Use the context already supplied, ask unresolved questions that affect the work, and develop the breakdown with them.
|
||||
|
||||
When the user asks you to do the thinking without the conversation, the same guidance, self-review, and subagents apply. Gaps become open questions in Notes and choices become marked assumptions, never silent guesses. The check on a draft in `references/validate.md` always runs. Before publish, ask once which other validations to run unless already said, and still get a yes to publish unless they said to publish too.
|
||||
|
||||
## Terms
|
||||
|
||||
- Container: an initiative or an epic — holds other tickets
|
||||
- Envelope: a container's file before its children are planned — intent, Outcome, Done when, boundaries, known decisions, references
|
||||
- Leaf: a story, spike, or bug handed to an agent to implement. Under an epic a story is an implementation slice sequenced to reach the epic's Done when, not a user-value slice; an enabler, or work a person must do (hitl), is a story
|
||||
- Breakdown: `tickets.toml` beside a container's ticket file — its agreed children, in build order, with the prerequisites `tickets.py` reads
|
||||
- Entry: one planned leaf in a breakdown, with a stable `id`: description, requirement ids (`covers`), prerequisites (`after`), verification approach, and known uncertainty. It needs no file to be built: the build reads the entry and its epic. With no file and no plan its state is `planned`
|
||||
- Pull: write an entry's leaf file with `tickets.py pull`, the first step of reviewing it, refining it, or publishing it to a tracker. Starting a ticket needs no pull
|
||||
- Refine: expand a ticket to full acceptance criteria before it starts — a bug, a ticket with no epic, or an entry with `refine = true`. Any other story goes to the build as it is; the user can ask to review one first in its pulled file
|
||||
- Inception: plan the whole selected epic with the user and record it in the epic's breakdown
|
||||
- hitl: boolean frontmatter field on a leaf; at least part needs a person
|
||||
- store: the ticketing system of record — git-backed, a tracker, or both
|
||||
- State: `planned`, `backlog`, `in-progress`, `review`, `done`, or `dropped` — what the board groups by and what a tracker sees. Every `tickets.py` row carries its ticket's `state`; use it rather than deriving it
|
||||
|
||||
## On activation
|
||||
|
||||
1. Resolve config: `uv run {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root} --key core.output_folder --key core.active_initiative`.
|
||||
- Script not found: BMad is not set up here. Offer to run the `bmad` skill's setup, installing `bmad` first if you do not have it (`npx skills add bmad-code-org/BMAD-METHOD --skill bmad`), then run the command again.
|
||||
|
||||
Tickets are drafted under `{output_folder}/{active_initiative}/` — an initiative folder, or a backlog folder scoped however the user wants. Unset: offer to create the initiative folder, or a backlog folder, and record it as `active_initiative` under `[core]` in `{project-root}/_bmad/custom/config.user.toml`.
|
||||
2. Read the store config: `uv run {skill-root}/scripts/read_store.py --project-root {project-root} -k tickets` — the store, how to reach it, and its type and status maps. A project that has set up no store uses the repo store, and this still answers.
|
||||
3. Resolve the customization: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} -k workflow`. A `{workflow.<key>}` in a reference is that key's value in this result.
|
||||
4. Run `{workflow.activation_steps_prepend}`. Treat `{workflow.persistent_facts}` as foundational context for the session: entries prefixed `file:` are paths or globs under `{project-root}` to load, the rest are facts verbatim.
|
||||
5. Run `{workflow.activation_steps_append}`. When the requested operation ends, run `{workflow.on_complete}`.
|
||||
|
||||
## Intake
|
||||
|
||||
For a request that creates work, size it from what the user said and what is in context, and say which path you are taking and why; the user overrides, and an override is a `Decision:` line.
|
||||
|
||||
- **Standalone** — one bug or story: `references/ticket.md`; a file in `backlog/`, or an entry in the epic the user names; no spec offered, the single-ticket check.
|
||||
- **Epic** — more than one ticket, all of which one epic holds: the epic path in `references/slice.md`.
|
||||
- **Initiative** — work that needs more than one epic: authored and split into epics per `references/slice.md`.
|
||||
|
||||
A spec folder handed over by `bmad-spec` is the epic's requirement source: `covers` cites its `CAP-N` ids and no spec is offered.
|
||||
|
||||
## Routing
|
||||
|
||||
| The user wants | Read |
|
||||
|---|---|
|
||||
| an initiative started or authored, split into epics; an epic incepted into stories or re-sliced | `{skill-root}/references/slice.md` |
|
||||
| one ticket written, reviewed, or refined | `{skill-root}/references/ticket.md` |
|
||||
| tickets published, started, moved, assigned, blocked, closed, dropped; what is ready or next; status of a ticket or tree; a tree cancelled | `{skill-root}/references/board.md` |
|
||||
| a ticket, a set, or a tree validated | `{skill-root}/references/validate.md` |
|
||||
| a tracker connected; the store reconfigured or switched | `{skill-root}/references/store-setup.md` |
|
||||
|
||||
`tickets.py` in this skill is `uv run {skill-root}/scripts/tickets.py --project-root {project-root}`. On the repo store, for "what is ready" or "what is the status", run `tickets.py next` or `tickets.py status` first and answer from it: it covers the active initiative and `backlog/`. Open `references/board.md` to act on a row, or when `problems`, `drift`, `unpinned_after`, `undeclared_after`, or `order_conflict` comes back with something in it. On a tracker, open `references/board.md` first.
|
||||
|
||||
## Store operations
|
||||
|
||||
`uv run {skill-root}/scripts/read_store.py --project-root {project-root} -k verbs.<name>` (repeat `-k`), then follow the verb as written. A verb holds the store's own commands; the rules every store shares are in `references/board.md`.
|
||||
|
||||
| Verb | For |
|
||||
|---|---|
|
||||
| `setup` | connect the tool; create what the maps name |
|
||||
| `write` | create or change a ticket — body, state, assignee, parent, blocking, fields |
|
||||
| `query` | one ticket, a container's children, a search |
|
||||
|
||||
A store call that fails: tell the user what is blocking, confirm the setup with them, and ask them to authenticate when that is the cause. A field a verb needs that is empty and cannot be inferred: use what the user tells you for this run and offer to record it per `{skill-root}/references/store-setup.md`.
|
||||
|
||||
## The ticket tree
|
||||
|
||||
```
|
||||
{output_folder}/
|
||||
{active_initiative}/ # example active_initiative=initiative-checkout
|
||||
initiative-checkout.md
|
||||
tickets.toml # the epics in build order
|
||||
spec-checkout/
|
||||
epic-cart-rules/
|
||||
epic-cart-rules.md
|
||||
tickets.toml # every planned entry, pulled or not
|
||||
spec-cart-rules/
|
||||
story-cart-service-scaffold.md # pulled; its id is in its frontmatter
|
||||
story-cart-service-scaffold-plan.md # the build's plan, with the ticket's status
|
||||
story-apply-discount-codes-plan.md # a plan for an entry with no file
|
||||
spike-discount-engine-latency.md
|
||||
backlog/
|
||||
bug-checkout-total-ignores-discount-codes.md
|
||||
```
|
||||
|
||||
- A container is a folder `<type>-<slug>/` holding its same-named ticket file, its `tickets.toml`, its spec, and its children. When an initiative has epics, every leaf is under one.
|
||||
- A leaf under a container is an entry in that `tickets.toml`; it gets a file `<type>-<slug>.md` beside it only when pulled. A leaf with no parent is a file in `backlog/`. Other skills' artifacts sit beside the ticket, named after it.
|
||||
- An entry's `id` is a number (or letters and digits, such as `6a`, when the work already has that id), assigned once and never reused; a ticket added to a folder takes `next_id` from `tickets.py status <folder>`. It is the ticket's only name on the repo store: `3` inside its epic, `2.3` from another epic. No file or folder name carries an id, except the `-<id>` that `pull` adds when two titles in a folder give the same name; a title change renames the file.
|
||||
- The order of tables in `tickets.toml` is the build order; `id` is not.
|
||||
- Once a leaf file exists it is truth: edit the file, and keep the entry's `id`, `type`, `title`, `after`, and `hitl` equal to it. The entry's other fields are not maintained after pull.
|
||||
- A leaf's `status` and blocked fields live in the build's plan, `<type>-<slug>-plan.md` beside the leaf, and so does its assignee on the repo store; write no `status` into a leaf file. `bmad-ticket` changes them only as `references/board.md` says, and the plan is never sent to a tracker.
|
||||
|
||||
By default the tree is the store. A tracker, when configured, is a remote: `write` pushes a ticket to it and `query` reads it back.
|
||||
|
||||
Resolve a ticket the user names — `1.2`, a tracker id, a file name, an unbroken phrase from its title — with `tickets.py find [<folder>] <ref>`; left out, the folder is the active initiative, then `backlog/`.
|
||||
|
||||
Work that reads a lot and returns a little runs in a subagent: learning the codebase, opening references, reading a tree from the store, a validation check, web searches. If the harness blocks subagents, say so and continue inline.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
id: [the entry's id in tickets.toml; the folder's `next_id` for a ticket with no entry] # tracker_id and remote are written at publish on a tracker
|
||||
type: bug
|
||||
title: "[What is wrong, from the user's view]"
|
||||
parent: [folder name of the epic, or of the initiative when there are no epics; none for a standalone bug in backlog/]
|
||||
covers: []
|
||||
after: [] # prerequisites only: a sibling's id; "<epic id>.<entry id>" in another epic; epic-<slug> for that whole epic
|
||||
assignee: "" # a tracker's assignee, written by `tickets.py mirror`; otherwise assignee, blocked_at, and blocked_reason live in the plan
|
||||
# status lives in the plan beside this file, not here; tracker_status by a tracker sync
|
||||
refined: false # true once the user approves its criteria
|
||||
hitl: false
|
||||
risk: [low|medium|high]
|
||||
severity: [P0|P1|P2|P3] # P0: outage, data loss, or security exposure. P1: core function broken, no workaround. P2: impaired, a workaround exists. P3: cosmetic
|
||||
---
|
||||
|
||||
<!-- The refined shape. A done or dropped ticket stays as it was written. -->
|
||||
|
||||
# [Title]
|
||||
|
||||
## Description
|
||||
|
||||
[What the user sees go wrong and what should happen instead, from their side. 2–4 sentences.]
|
||||
|
||||
## Reproduction
|
||||
|
||||
[Exact steps, environment, and what happens versus what should happen. Logs or errors verbatim.]
|
||||
|
||||
## Cause Hypothesis
|
||||
|
||||
[Where the defect likely lives and why — a hypothesis, never a prescribed fix.]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **[The expected behavior holds]**
|
||||
**Given** [the state from the reproduction]
|
||||
**When** [the reproduction steps are followed]
|
||||
**Then** [what should happen, as it should now happen]
|
||||
2. **Tests cover the condition found and fixed**
|
||||
**Given** the test suite
|
||||
**When** it runs
|
||||
**Then** a test that fails on the defect and passes on the fix covers the reproduction, and one covers each related case the fix touched
|
||||
3. **Or: no change is needed, with proof**
|
||||
**Given** the reproduction
|
||||
**When** it is run on the current code
|
||||
**Then** the expected behavior already holds, or the report was mistaken, with the evidence recorded in Notes — this supersedes 1 and 2
|
||||
|
||||
## References
|
||||
|
||||
- parent — [path or url, or none]
|
||||
- [source — path, section]
|
||||
- [logs, screenshots, or sample data — location]
|
||||
|
||||
## Notes
|
||||
|
||||
[Only what is not in the repo or the source, and what is not settled. Cut if empty.]
|
||||
|
||||
- Decision: [a choice the user made, dated]
|
||||
- Assumption: [a choice made while drafting that the user has not confirmed]
|
||||
- Open question: [what is not settled; answering it is part of the ticket's work]
|
||||
|
||||
<!-- Example, not part of the ticket: match its level of detail. What is good here: a reproduction someone else can follow, with the actual and expected values; a cause hypothesis that is not a fix; one criterion for the behavior, one for the tests that cover the condition found and fixed, and one that supersedes both when the reproduction shows no change is needed. -->
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: 7
|
||||
type: bug
|
||||
title: "Checkout total ignores an applied discount code after the shopper changes quantity"
|
||||
parent: none
|
||||
covers: []
|
||||
refined: true
|
||||
hitl: false
|
||||
risk: medium
|
||||
severity: P2
|
||||
---
|
||||
|
||||
# Checkout total ignores an applied discount code after the shopper changes quantity
|
||||
|
||||
## Description
|
||||
|
||||
A shopper applies a valid code, then changes an item's quantity, and the total goes back to full price while the discount line still shows the code. They are charged full price at payment.
|
||||
|
||||
## Reproduction
|
||||
|
||||
1. Production, Chrome 130, logged out. Add "Canvas tote" (29.00) ×1 to the cart.
|
||||
2. Apply SAVE10. Total shows 26.10, discount line shows SAVE10 −2.90.
|
||||
3. Change quantity to 2.
|
||||
4. Actual: total 58.00, discount line still SAVE10 −2.90. Expected: 52.20, discount line SAVE10 −5.80.
|
||||
5. Continue to payment: amount charged is 58.00.
|
||||
Log line at step 3: `pricing.recompute cart=… discount=null`.
|
||||
|
||||
## Cause Hypothesis
|
||||
|
||||
The quantity-change path recomputes the total without passing the applied code, so the discount is dropped while the UI keeps the stale line. Likely in the recompute call, not in the discount engine, since step 2 is right.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **The expected behavior holds**
|
||||
**Given** a cart with a valid code applied
|
||||
**When** the shopper changes an item's quantity
|
||||
**Then** the total recomputes with the code still applied and the discount line shows the new amount
|
||||
2. **Tests cover the condition found and fixed**
|
||||
**Given** the pricing test suite
|
||||
**When** it runs
|
||||
**Then** a test that fails on the defect and passes on the fix covers a quantity change with a code applied, and one covers removing an item with a code applied
|
||||
3. **Or: no change is needed, with proof**
|
||||
**Given** the reproduction
|
||||
**When** it is run on the current code
|
||||
**Then** the total already recomputes with the code applied, or the report was mistaken, with the evidence recorded in Notes — this supersedes 1 and 2
|
||||
|
||||
## References
|
||||
|
||||
- parent — none
|
||||
- logs — support ticket #4471, attachment pricing.log
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
tracker_id: "" # with remote (the url), written at publish on a tracker; cut on the repo store
|
||||
key: "" # tracker project or team for everything under this, when it differs from the store's
|
||||
type: epic
|
||||
title: "[The outcome this container exists to reach]"
|
||||
parent: [folder name of the initiative]
|
||||
covers: [parent capability ids this epic owns; keep these when adding an epic-local spec; a constraint is cited in References, not covered]
|
||||
after: [] # epics this whole container waits on; the order of epics is in the initiative's tickets.toml
|
||||
assignee: "" # status is added when work starts (in-progress | done | dropped)
|
||||
risk: [low|medium|high — the highest expected among its children]
|
||||
---
|
||||
|
||||
<!-- At initiative slicing, the envelope: frontmatter, Description, Outcome, Done when, Boundaries, References, and known Notes. Requirements are completed at this epic's inception, and its children are planned in `tickets.toml` beside this file. -->
|
||||
|
||||
# [Title]
|
||||
|
||||
## Description
|
||||
|
||||
[What is true when this is done and why it matters. With a spec, one paragraph pointing at it; without one, the intent in full.]
|
||||
|
||||
## Outcome
|
||||
|
||||
[One sentence: for whom, what changes, and the signal that shows it worked — the spec's, named not restated, when there is one.]
|
||||
|
||||
## Requirements
|
||||
|
||||
[The requirement source at this altitude: the source's lines as stable ids for children to cite, each mapping to a parent id in covers. Reuse the parent's ids when the lines are the parent's; when the epic splits or adds one, mint `E<n> (R<m>)` so the map is on the line. An epic with no parent ids, such as the platform baseline, leaves covers empty and cites the source section on each line instead. A referenced numbered source replaces it; a separate spec only when the source outgrows this section.]
|
||||
|
||||
## Done when
|
||||
|
||||
[Three to six checks the owner runs at the end, taken from the source's measures and limits. Closing every child is not one.]
|
||||
|
||||
## Boundaries
|
||||
|
||||
[Which boundary this container follows — team, service, UI, capability — and what it is not. Point at the spec's non-goals when there is one.]
|
||||
|
||||
## References
|
||||
|
||||
[`type — location, section`. The spec at this level when one exists (its references are followed from there); otherwise what this was split from.]
|
||||
|
||||
- parent — [path or url, section containing the upstream requirement ids]
|
||||
- spec — [local spec when present; its capability ids map back to parent ids]
|
||||
- constraint — [the source section that binds this container: privacy, platform, licensing, performance]
|
||||
- [an input the spec does not carry — location, section]
|
||||
|
||||
## Notes
|
||||
|
||||
[What is not settled at this level. Cut if empty.]
|
||||
|
||||
- Assumption: [a choice made while slicing that the user has not confirmed]
|
||||
- Open question: [what the source does not settle and which children wait on it]
|
||||
- Unknown: [what is not yet known and which entries wait on it, recorded now so it is not found late]
|
||||
- Parked: [a requirement id not placed on any child, and why, with the user's knowledge]
|
||||
- Decision: [a choice the user made, dated, so it is not asked again — a declined suggestion belongs here too]
|
||||
- Source conflict: [id or section — what the source says vs what the code or another source shows]
|
||||
- Waits on [epic] because: [the one-line reason for each entry in after]
|
||||
|
||||
<!-- Example, not part of the ticket: an incepted epic with no spec of its own. The parent assigned R1–R4 to this epic from an unnumbered PRD; Requirements records those lines using the same ids. Done when holds deliverable checks. Notes holds decisions and unknowns. -->
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: epic
|
||||
title: "Shoppers manage their cart"
|
||||
parent: initiative-checkout
|
||||
covers: [R1, R2, R3, R4]
|
||||
assignee: ""
|
||||
risk: medium
|
||||
---
|
||||
|
||||
# Shoppers manage their cart
|
||||
|
||||
## Description
|
||||
|
||||
A shopper adds items, changes quantities, applies discount codes, and recovers from every refusal with a reason — and the total they see is always the one the payment step receives.
|
||||
|
||||
## Outcome
|
||||
|
||||
Shoppers who reach the cart continue to payment more often because the total never surprises them; the PRD's cart-to-payment completion measure is the signal.
|
||||
|
||||
## Requirements
|
||||
|
||||
- R1: Add, remove, and change quantity of any item; the total updates at once. (PRD, Capabilities, Cart)
|
||||
- R2: Apply one discount code; refused codes say why. (PRD, Capabilities, Cart)
|
||||
- R3: The total shown is the total charged, before tax. (PRD, Capabilities, Cart)
|
||||
- R4: Cart actions respond within 300 ms on the slowest supported device. (PRD, Constraints)
|
||||
|
||||
## Done when
|
||||
|
||||
1. R1–R3 work end to end on the live site, refusals included, with the reason shown.
|
||||
2. The end-to-end suite proves the shown total equals the amount sent to payment on every cart path.
|
||||
3. R4 holds on the slowest supported device under the load test.
|
||||
4. A shopper with no code applied sees no change from today's cart.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The cart UI. Not the pricing service (epic Pricing rules), not tax (epic Tax and payment).
|
||||
|
||||
## References
|
||||
|
||||
- prd — _bmad-output/initiative-checkout/prd-checkout/prd-checkout.md, sections Capabilities and Constraints
|
||||
- design — https://figma.com/design/ab12cd/checkout, frame Cart
|
||||
|
||||
## Notes
|
||||
|
||||
- Decision: codes are case-insensitive (user's decision, 2026-08-12).
|
||||
- Unknown: whether the discount engine can validate a code within R4; entry 04 waits on it.
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
tracker_id: "" # with remote (the url), written at publish on a tracker; cut on the repo store
|
||||
key: "" # tracker project or team for everything under this, when it differs from the store's
|
||||
type: initiative
|
||||
title: "[The outcome this container exists to reach]"
|
||||
parent: none
|
||||
covers: [capability ids from the spec at this level, or from Requirements below when the source has none; a constraint is cited in References, not covered]
|
||||
after: [] # epics this whole container waits on; the order of epics is in the initiative's tickets.toml
|
||||
assignee: "" # status is added when work starts (in-progress | done | dropped)
|
||||
risk: [low|medium|high — the highest expected among its children]
|
||||
---
|
||||
|
||||
# [Title]
|
||||
|
||||
## Description
|
||||
|
||||
[What is true when this is done and why it matters. With a spec, one paragraph pointing at it; without one, the intent in full.]
|
||||
|
||||
## Outcome
|
||||
|
||||
[One sentence: for whom, what changes, and the signal that shows it worked — the spec's, named not restated, when there is one.]
|
||||
|
||||
## Requirements
|
||||
|
||||
[The requirement source at this altitude: the source's lines as stable ids for children to cite, each mapping to a source id in covers. A referenced numbered source replaces it; a separate spec only when the source outgrows this section.]
|
||||
|
||||
## Done when
|
||||
|
||||
[Three to six checks the owner runs at the end, taken from the source's measures and limits. Closing every child is not one.]
|
||||
|
||||
## Boundaries
|
||||
|
||||
[Which boundary this container follows — team, service, UI, capability — and what it is not. Point at the spec's non-goals when there is one. Then one line per unit the work touches that gets no epic, and the tracer path across epics in one sentence.]
|
||||
|
||||
- Touch point: [unit] — [what is consumed or configured there]; owner: epic-[slug]
|
||||
|
||||
## References
|
||||
|
||||
[`type — location, section`. The spec at this level when one exists (its references are followed from there); otherwise what this was split from.]
|
||||
|
||||
- spec — [path from {project-root}, or url for a remote source; section Capabilities]
|
||||
- constraint — [the source section that binds this container: privacy, platform, licensing, performance]
|
||||
- [an input the spec does not carry — location, section]
|
||||
|
||||
## Notes
|
||||
|
||||
[What is not settled at this level. Cut if empty.]
|
||||
|
||||
- Assumption: [a choice made while slicing that the user has not confirmed]
|
||||
- Open question: [what the source does not settle and which children wait on it]
|
||||
- Unknown: [what is not yet known and which entries wait on it, recorded now so it is not found late]
|
||||
- Parked: [a requirement id not placed on any child, and why, with the user's knowledge]
|
||||
- Decision: [a choice the user made, dated, so it is not asked again — a declined suggestion belongs here too]
|
||||
- Source conflict: [id or section — what the source says vs what the code or another source shows]
|
||||
- Waits on [epic] because: [the one-line reason for each entry in after]
|
||||
|
||||
<!-- An initiative is the business outcome its epics serve, tied to a company goal, usually spanning quarters and more than one boundary. For a solo developer or small team with no larger goal above it, a whole product is a fine initiative.
|
||||
Example, not part of the ticket: match its level of detail. This one keeps a separate spec because its source outgrew the section: Description points at it, covers cites its ids, Requirements is cut, Outcome names its signal. Done when reads as business outcomes a product owner checks at the end, not deliverables. The epic order, with what each needs from the one before, is in `tickets.toml` beside this file. -->
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: initiative
|
||||
title: "Checkout that shoppers finish"
|
||||
parent: none
|
||||
covers: [C1, C2, C3, C4, C5, C6, P1, P2, T1]
|
||||
assignee: ""
|
||||
risk: high
|
||||
---
|
||||
|
||||
# Checkout that shoppers finish
|
||||
|
||||
## Description
|
||||
|
||||
Shoppers go from cart to paid order in one pass, with the price they saw, on any supported device or as a guest. The spec owns the capabilities, constraints, and non-goals; this initiative delivers them for the fall release.
|
||||
|
||||
## Outcome
|
||||
|
||||
The Q4 revenue target depends on lifting cart-to-payment completion from 55% to 70%; this initiative owns that number, the spec's success signal, measured over a full quarter.
|
||||
|
||||
## Done when
|
||||
|
||||
1. Cart-to-payment completion holds at the spec's target for one full quarter after release.
|
||||
2. Every capability in C1–C6, P1–P2, and T1 is live for all shoppers, not behind a flag.
|
||||
3. Refund and chargeback rates are no worse than the quarter before release.
|
||||
4. No P0 or P1 checkout bug open for more than a day during the first month.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The web store's checkout flow, the payment-provider integration, and guest checkout across web and mobile. Not the order-management backend beyond the order it creates, not loyalty, not the catalog; see the spec's non-goals. Tracer path: one catalog item priced, carted, taxed, and paid by a signed-in shopper.
|
||||
|
||||
- Touch point: notification service — a new order-confirmation template, no code change; owner: epic-tax-and-payment
|
||||
|
||||
## References
|
||||
|
||||
- spec — _bmad-output/initiative-checkout/spec-checkout/spec-checkout.md
|
||||
- constraint — the same spec, section Constraints, PCI scope and response time
|
||||
- prd — _bmad-output/initiative-checkout/prd-checkout/prd-checkout.md, for history only
|
||||
|
||||
## Notes
|
||||
|
||||
- Parked: gift cards (C7); not in the fall release, user's call 2026-08-12.
|
||||
- Decision: one payment provider for v1, replacing the current one (user's decision, 2026-08-12).
|
||||
- Unknown: whether the tax service can meet the response-time constraint at peak; epic Tax and payment owns the answer.
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
id: [the entry's id in tickets.toml; the folder's `next_id` for a ticket with no entry] # tracker_id and remote are written at publish on a tracker
|
||||
type: spike
|
||||
title: "[The question this answers]"
|
||||
parent: [folder name of the epic, or of the initiative when there are no epics; none for a standalone ticket in backlog/]
|
||||
covers: []
|
||||
after: [] # prerequisites only: a sibling's id; "<epic id>.<entry id>" in another epic; epic-<slug> for that whole epic
|
||||
assignee: "" # a tracker's assignee, written by `tickets.py mirror`; otherwise assignee, blocked_at, and blocked_reason live in the plan
|
||||
# status lives in the plan beside this file, not here; tracker_status by a tracker sync
|
||||
refined: false # true once the user approves its full criteria; a pulled spike has this line only when its entry says `refine = true`
|
||||
hitl: true
|
||||
risk: [low|medium|high]
|
||||
---
|
||||
|
||||
<!-- The refined shape. A done or dropped ticket stays as it was written. -->
|
||||
|
||||
# [Title]
|
||||
|
||||
## Description
|
||||
|
||||
[The question, why it is open, and which tickets wait on the answer.]
|
||||
|
||||
## Approach
|
||||
|
||||
[The steps, as many as the question needs: what to research, what to design, what to prototype, who signs off. The time box. What a good-enough answer looks like.]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **[The candidates are compared on the criteria that decide]**
|
||||
Verify: [where the comparison is recorded]
|
||||
2. **[The prototype shows the chosen answer works for the cases that matter]**
|
||||
Verify: [where the prototype and its results live]
|
||||
3. **[The answer is recorded, with its consequences, where the waiting tickets can find it]**
|
||||
Verify: [where]
|
||||
4. **[The people who must accept it have]**
|
||||
Verify: [who, recorded where]
|
||||
|
||||
## References
|
||||
|
||||
- parent — [path or url]
|
||||
- [source — path, section]
|
||||
|
||||
## Notes
|
||||
|
||||
[Assumptions and open questions about the spike itself, each marked. Cut if empty.]
|
||||
|
||||
<!-- Example, not part of the ticket: match its level of detail. What is good here: the unknown was a placeholder in the architecture; the spike names who waits, runs research, design, prototype, and sign-off in a time box, says what good enough is, and records the answer where the stories will find it. hitl because people accept the finding. -->
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: 2
|
||||
type: spike
|
||||
title: "How do field devices merge conflicting observations after days offline?"
|
||||
parent: epic-field-sync
|
||||
covers: []
|
||||
refined: true
|
||||
hitl: true
|
||||
risk: low
|
||||
---
|
||||
|
||||
# How do field devices merge conflicting observations after days offline?
|
||||
|
||||
## Description
|
||||
|
||||
The architecture left sync as a placeholder: "devices reconcile on reconnect." Researchers in the field edit the same observation records on several tablets for days without a link, then sync over a slow satellite connection. Three planned stories in this epic — edit observations offline, sync on reconnect, show merge results to the team lead — cannot be refined for implementation until we know how conflicting edits merge, what that does to the data model, and whether the link can carry it.
|
||||
|
||||
## Approach
|
||||
|
||||
Time box: four days. At the end, the best answer so far is recorded, not a perfect one.
|
||||
|
||||
1. Research: compare three ways to merge — a CRDT library, last-writer-wins per field with a conflict log, and a custom merge on the observation schema — on the criteria that decide: correctness on the six conflict cases the field team listed, payload size over a 2 kbps link, library maturity, and how much of the data model each forces us to change.
|
||||
2. Design: a one-page note on the chosen approach, its data-model consequences, and the cases it does not resolve automatically.
|
||||
3. Prototype: two tablets and a simulated link; run the six conflict cases and measure sync payload and time.
|
||||
4. Sign-off: the lead engineer accepts the design; the field science lead accepts how unresolved conflicts are shown to people.
|
||||
|
||||
Good enough: all six cases merge as the field team expects, or the exceptions are listed with how a person resolves them, and a full day's edits sync in under ten minutes on the simulated link.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **The three approaches are compared on the deciding criteria**
|
||||
Verify: the comparison table is in epic-field-sync/spike-04-offline-merge-comparison.md
|
||||
2. **The prototype merges the six conflict cases on a simulated link**
|
||||
Verify: the prototype and its run log are under epic-field-sync/spike-04-offline-merge-prototype/, with payload and time per case
|
||||
3. **The answer and its data-model consequences are recorded where the stories will find them**
|
||||
Verify: the design note is epic-field-sync/spike-04-offline-merge-design.md and the choice is in epic-field-sync.md Notes as a decision
|
||||
4. **The lead engineer and the field science lead have accepted it**
|
||||
Verify: both named in that decision line with the date
|
||||
|
||||
## References
|
||||
|
||||
- parent — epic-field-sync/epic-field-sync.md
|
||||
- architecture — docs/architecture.md, section Sync (the placeholder)
|
||||
- field team conflict cases — docs/research/conflict-cases.md
|
||||
|
||||
## Notes
|
||||
|
||||
- Assumption: the satellite link is never better than 2 kbps up; confirm with the operations lead.
|
||||
- Open question: is the conflict log kept forever, or pruned after the lead resolves it? The field science lead answers.
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: [the entry's id in tickets.toml; the folder's `next_id` for a ticket with no entry] # tracker_id and remote are written at publish on a tracker
|
||||
type: story
|
||||
title: "[What exists or works when this is done]"
|
||||
parent: [folder name of the epic, or of the initiative when there are no epics; none for a standalone ticket in backlog/]
|
||||
covers: [ids from the epic's spec, referenced numbered source, or Requirements; the ones this ticket delivers toward]
|
||||
after: [] # prerequisites only: a sibling's id; "<epic id>.<entry id>" in another epic; epic-<slug> for that whole epic
|
||||
assignee: "" # a tracker's assignee, written by `tickets.py mirror`; otherwise assignee, blocked_at, and blocked_reason live in the plan
|
||||
# status lives in the plan beside this file, not here; tracker_status by a tracker sync
|
||||
refined: false # true once the user approves its full criteria; a pulled story has this line only when its entry says `refine = true`
|
||||
hitl: false
|
||||
risk: [low|medium|high]
|
||||
---
|
||||
|
||||
<!-- Pulled: a one-sentence Description, Acceptance Criteria as one Verify: line, References, local Notes. Reviewed: the same, gone over with the user. Refined, with numbered criteria and Boundaries: only a ticket with no epic, an entry with `refine = true`, or on request. A done or dropped ticket stays as it was written. -->
|
||||
|
||||
# [Title]
|
||||
|
||||
## Description
|
||||
|
||||
[One sentence, what this delivers; gone over with the user at review. With numbered criteria: what exists or works when this is done and how it advances the epic, 2–4 sentences; the criteria carry the proof, do not restate them.]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
[One line, `Verify: how it will be checked`, nothing else. No epic, `refine = true`, or on request: the numbered criteria below instead.]
|
||||
|
||||
1. **[Short name of the behavior]**
|
||||
**Given** [the state before: data, user, config]
|
||||
**When** [the one action]
|
||||
**Then** [what is observed: a response, a record, a screen — something a person or a test can check]
|
||||
**And** [a further observation from the same action, when there is one]
|
||||
2. **[Short name of the behavior]**
|
||||
**Given** [...]
|
||||
**When** [...]
|
||||
**Then** [...]
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Must not change: [adjacent behavior a valid implementation could damage — name behavior, not files]
|
||||
|
||||
## References
|
||||
|
||||
- parent — [path or url]
|
||||
- [source document — path, section]
|
||||
- [design or prototype — path, for UI work]
|
||||
|
||||
## Notes
|
||||
|
||||
[Only what is not in the repo or the source, and what is not settled. Cut if empty.]
|
||||
|
||||
- [A frozen interface, a declined option]
|
||||
- Decision: [a choice the user made, dated]
|
||||
- Assumption: [a choice made while drafting that the user has not confirmed; confirmed, it becomes a Decision line]
|
||||
- Open question: [what only this ticket waits on, answered as part of its work; one that touches siblings goes in the parent's Notes]
|
||||
|
||||
<!-- Example, not part of the ticket: match its level of detail. What is good here: the Description is what the shopper can do, end to end; every criterion states a rule, not an instance, with its failure path; Boundaries names behavior, not files; References points at the nearest document; Notes holds only what is not in the repo or the source, plus one assumption for the user to confirm. Numbered criteria because its entry says `refine = true`. -->
|
||||
|
||||
```markdown
|
||||
---
|
||||
id: 4
|
||||
type: story
|
||||
title: "A shopper applies a discount code and sees the new total"
|
||||
parent: epic-cart-rules
|
||||
covers: [R2, R3]
|
||||
after: [3]
|
||||
refined: true
|
||||
hitl: false
|
||||
risk: medium
|
||||
---
|
||||
|
||||
# A shopper applies a discount code and sees the new total
|
||||
|
||||
## Description
|
||||
|
||||
A shopper with items in the cart enters a discount code, and the cart total updates to show the discount before tax. An invalid or expired code tells them why it was refused and leaves the total as it was.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **Valid code reduces the total**
|
||||
**Given** a cart and a valid discount code
|
||||
**When** the shopper applies it
|
||||
**Then** the total drops by the code's value and a discount line shows the code and the amount
|
||||
2. **Expired code is refused with the reason**
|
||||
**Given** a cart and an expired code
|
||||
**When** the shopper applies it
|
||||
**Then** the total is unchanged and the message says the code expired and when
|
||||
3. **Unknown code is refused without revealing valid codes**
|
||||
**Given** a cart and a code that does not exist
|
||||
**When** the shopper applies it
|
||||
**Then** the total is unchanged and the message says the code is not recognized, nothing more
|
||||
4. **An applied code survives a quantity change**
|
||||
**Given** an applied code
|
||||
**When** the shopper changes an item's quantity
|
||||
**Then** the total recomputes with the code still applied
|
||||
5. **Payment receives the discounted total**
|
||||
**Given** an applied code
|
||||
**When** the shopper continues to payment
|
||||
**Then** the amount sent to payment equals the total shown
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Must not change: catalog prices, tax computation, the cart for shoppers who apply no code.
|
||||
|
||||
## References
|
||||
|
||||
- parent — _bmad-output/initiative-checkout/epic-cart-rules/epic-cart-rules.md, Requirements R2 and R3
|
||||
- design — https://figma.com/design/ab12cd/checkout, frame Cart
|
||||
|
||||
## Notes
|
||||
|
||||
- Decision: the discount engine's `validate(code, cart) -> {amount, reason}` interface is frozen (2026-08-12).
|
||||
- Assumption: the refusal messages above are final copy; no design text exists for them.
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# tickets.toml — a container's agreed children. tickets.py reads it.
|
||||
# The order of the tables is the build order: move a table to reorder. `id` names a child for good and is never reused; it is not an order.
|
||||
# An epic's file holds [[entry]] tables. An initiative's holds [[epic]] tables; with no epics it holds [[entry]] tables.
|
||||
# Keys tickets.py does not know are ignored, so a team may add its own.
|
||||
# Status never lives here: an entry with no plan and no leaf file is planned; its plan beside it carries `status`, and on a tracker `tickets.py mirror` writes `tracker_status` in the leaf file.
|
||||
|
||||
# ---- In an epic folder: one table per planned story, spike, or bug ----
|
||||
|
||||
[[entry]]
|
||||
id = 1 # the leaf file carries it as `id:`; refer to it as 1 here, as <epic id>.1 from another epic
|
||||
type = "story" # story | spike | bug
|
||||
title = "Cart service scaffold"
|
||||
description = "Stands up the cart service with one add-item path through UI, API, and store."
|
||||
verify = "A shopper adds one item on the deployed site and sees it in the cart."
|
||||
covers = ["R1"] # ids from the epic's requirement source
|
||||
after = [] # real prerequisites only, never the sequence; empty means it can run beside anything above it. A bare number is a sibling's id; everything else is quoted
|
||||
hitl = true # a person runs the starter
|
||||
risk = "low" # low | medium | high
|
||||
# estimate = 2 # only when the user gives one; never proposed
|
||||
# unknown = "..." # a question to settle with the user before this entry starts, one sentence; `tickets.py next` lists the entry under blocked until it is removed
|
||||
# references = ["architecture-checkout/architecture-checkout.md#ad-8"] # what this entry needs beyond the epic's own References; pull writes them to the ticket
|
||||
# notes = ["..."] # the user's words only, never filled by the agent; pull writes them to the ticket's Notes
|
||||
# refine = true # refine to full acceptance criteria before it starts
|
||||
# plan_checkpoint = true # a person reviews the ticket, or the builder's plan, before work starts
|
||||
# done_checkpoint = true # work pauses after this ticket until a person says continue
|
||||
|
||||
[[entry]]
|
||||
id = 2
|
||||
type = "story"
|
||||
title = "Apply and refuse discount codes"
|
||||
description = "Adds code validation to the pricing call and shows the refusal reason."
|
||||
verify = "A valid code lowers the total; an expired one is refused with its reason."
|
||||
covers = ["R2", "R3"]
|
||||
after = [1, "1.3"] # a sibling's id; "<epic id>.<entry id>" for an entry in another epic; "epic-<slug>" for that whole epic
|
||||
hitl = false
|
||||
risk = "medium"
|
||||
|
||||
# ---- In an initiative folder: one table per epic; table order is the build order ----
|
||||
|
||||
[[epic]]
|
||||
id = 1 # entries in other epics refer to this epic's entries as 1.<entry id>
|
||||
slug = "epic-pricing-rules" # the epic's folder name
|
||||
title = "Pricing rules"
|
||||
covers = ["P1", "P2"]
|
||||
|
||||
[[epic]]
|
||||
id = 2
|
||||
slug = "epic-cart-rules"
|
||||
title = "Shoppers manage their cart"
|
||||
covers = ["C1", "C2", "C3"]
|
||||
after = [{ epic = 1, needs = "the one-function pricing contract" }] # what this epic needs from which; the entries that wait on it carry the `after`
|
||||
@@ -0,0 +1,4 @@
|
||||
[skill]
|
||||
bmod = "bmod-method"
|
||||
source = "github:bmad-code-org/BMAD-METHOD/skills"
|
||||
scripts = ["scripts/tickets.py"]
|
||||
@@ -0,0 +1,81 @@
|
||||
# Store starter: GitHub Issues. `scripts/read_store.py` reads this file as the base layer; the project's
|
||||
# _bmad/custom/ticketing-store-config.toml names the store, fills the empty fields below, and replaces
|
||||
# any other key here, key by key.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# One tool: the gh CLI, 2.94 or later (sub-issues and dependencies). Labels carry type and status,
|
||||
# sub-issues carry hierarchy, blocked-by relations carry blocking, milestones put initiatives on the roadmap.
|
||||
# Using the defaults below: the repo needs the type, status, and hitl labels; `setup` creates them.
|
||||
|
||||
description = "GitHub Issues through the gh CLI: labels, sub-issues, blocked-by, milestones. For teams whose code and planning already live on GitHub."
|
||||
|
||||
[tickets]
|
||||
store = "github"
|
||||
key = "" # ids are the repo's issue numbers; leave empty
|
||||
repo = "" # OWNER/REPO; empty = the repo of the current checkout (origin)
|
||||
access = """
|
||||
Use the gh CLI against `repo`, passing `-R <repo>` on every call when it is set. Needs gh 2.94+; check `--help` when a flag is doubted.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
risk is a label risk:<value>; severity is a label P0-P3. `setup` creates them with the other
|
||||
labels; publish and update set them like any label. estimate is a label est:<value> (`setup` creates none; add on first use). Empty field: no label.
|
||||
"""
|
||||
|
||||
reference = """
|
||||
GitHub renders urls as links; nothing more to do.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Connect, unless `gh auth status` already shows a login: `gh auth login`.
|
||||
Labels must exist before they are used. List the repo's labels (`gh label list --limit 1000 --json name`)
|
||||
and run `gh label create <name>` for each of these it lacks, comparing names ignoring case: the
|
||||
[tickets.types] values except initiative (a milestone), the [tickets.status] values except done and
|
||||
dropped (closed states), `hitl`, risk:low, risk:medium, risk:high, and P0-P3. A create that fails with
|
||||
"already exists" found the label after all: leave it. Never `--force`: it recolours a label the repo
|
||||
already has.
|
||||
"""
|
||||
|
||||
write = """
|
||||
One issue per new ticket. tracker_id is the issue number and remote its url.
|
||||
Create with content only: `gh issue create --title <t> --body-file <b> --label <type>,<[tickets.status].<state>>[,hitl,risk:<r>,P<n>]
|
||||
[--milestone <initiative>]`, and mirror tracker_id and remote from the url it prints before anything else. Then add the
|
||||
relations with `gh issue edit <n> --parent <n> --add-blocked-by <n,n>`, leaving out any that `gh issue view <n> --json
|
||||
parent,blockedBy` already shows, since adding one twice fails. Keep --parent and --blocked-by off the create: gh adds them
|
||||
after the issue exists, and when that step fails it exits non-zero without printing the url, so a rerun files the ticket
|
||||
twice. A create that fails with no url may still have made the issue: before creating again, list the newest issues
|
||||
(`gh issue list --state all --limit 100 --json number,title,body,createdAt`) twice, a few seconds apart since the list can
|
||||
trail a create, and take the one whose title and body are what was sent. None matches and the list does not reach back to
|
||||
the failed create: ask before creating again. Changes go through `gh issue edit <n>` (--body-file, --add-assignee,
|
||||
--add-blocked-by, label swaps; --add-sub-issue on the parent). No parent: omit --parent; no initiative: omit --milestone.
|
||||
Status is a label swap on an open issue. done: remove the status label and `gh issue close <n>`;
|
||||
dropped: the same with `--reason "not planned"`. An initiative is a
|
||||
milestone (`gh api repos/{owner}/{repo}/milestones -f title=<name>`) holding its tree's issues.
|
||||
"""
|
||||
|
||||
query = """
|
||||
One issue: `gh issue view <n> --json
|
||||
number,title,labels,assignees,state,stateReason,milestone,parent,blockedBy,subIssuesSummary,body`.
|
||||
Type, hitl, risk, severity, and status are labels — except done and dropped, the closed states per
|
||||
[tickets.status]; after is blockedBy; the body's covers holds the source requirement ids. Children: `gh issue view <n>
|
||||
--json subIssues`. Words: `gh issue list --search "<words>" --state all`, narrowed by
|
||||
--label. An initiative's issues are the ones in its milestone; a standalone ticket has no milestone.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type.
|
||||
[tickets.types]
|
||||
initiative = "milestone" # an initiative is a milestone, not an issue; its epics carry it
|
||||
epic = "epic"
|
||||
story = "story"
|
||||
spike = "spike"
|
||||
bug = "bug"
|
||||
|
||||
# BMad state -> this store's status.
|
||||
[tickets.status]
|
||||
backlog = "backlog"
|
||||
in-progress = "in-progress"
|
||||
review = "review"
|
||||
done = "closed" # state, not a label
|
||||
dropped = "closed: not planned" # state, not a label
|
||||
@@ -0,0 +1,83 @@
|
||||
# Store starter: Jira Cloud. `scripts/read_store.py` reads this file as the base layer; the project's
|
||||
# _bmad/custom/ticketing-store-config.toml names the store, fills the empty fields below, and replaces
|
||||
# any other key here, key by key.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# One tool: acli, Atlassian's official CLI (`acli jira workitem ...`): create with --parent, transition,
|
||||
# links, comments, assign, JQL. The Atlassian Rovo MCP server (https://mcp.atlassian.com/v1/mcp/authv2)
|
||||
# is the alternative for an org that prefers MCP; it has no link-creation tool.
|
||||
|
||||
description = "Jira Cloud through acli: work types, transitions, links, JQL. For teams already on Jira, usually with an admin who can adjust the project workflow."
|
||||
|
||||
[tickets]
|
||||
store = "jira"
|
||||
key = "" # the default Jira project key, e.g. "SHOP"; Jira assigns the number. A container's own `key` overrides it for everything under it
|
||||
site = "" # https://yourorg.atlassian.net
|
||||
access = """
|
||||
Use acli against `site`. Custom fields go through `--from-json`; check a subcommand's spelling
|
||||
with `acli jira workitem --help` before first use.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
severity is Jira's priority: P0 = Highest, P1 = High, P2 = Medium, P3 = Low; set it at create
|
||||
(--priority, or --from-json when the flag is absent) and change it the same way. risk is a label
|
||||
risk-low, risk-medium, or risk-high. estimate is the Story Points field on a story and a label est:<value> on an epic, through --from-json. Empty field: default priority, no label.
|
||||
"""
|
||||
|
||||
reference = """
|
||||
acli creates no web links; the description line is the reference.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Connect, unless acli is already installed and signed in to `site`: install acli (on macOS,
|
||||
`brew tap atlassian/homebrew-acli && brew install acli`; elsewhere, per Atlassian's acli install
|
||||
guide), then `acli jira auth login --web --site <site>`.
|
||||
acli cannot create work types or statuses; those live in the project's settings. At first publish,
|
||||
read an existing work item (`acli jira workitem view <key> --json --fields issuetype,status`) and
|
||||
the project's work types, compare with [tickets.types] and [tickets.status], and tell the user
|
||||
exactly what to add in Jira or which map entry to set in the project's store config. Jira's defaults (To Do, In
|
||||
Progress, Done; resolution Won't Do) need no setup; [tickets.status] also expects an In Review status.
|
||||
Do not ask for a level above Epic: by default the initiative stays local and everything below it
|
||||
publishes. A team-managed project's admin adds a status as a board column under
|
||||
Project settings, Board; a company-managed project needs a Jira admin to add it to the workflow.
|
||||
"""
|
||||
|
||||
write = """
|
||||
The project is the ticket's own `key`, else that of the nearest container above it, else `key` here.
|
||||
tracker_id is the work item's key and remote its url.
|
||||
Create:
|
||||
`acli jira workitem create --project <key> --type <per [tickets.types]> --summary <t>
|
||||
--description-file <b> --parent <key> --label <type>[,hitl,risk-<risk>]`, priority per `fields` on
|
||||
a bug. Changes: `edit` (body, labels), `assign`, `transition --status <[tickets.status].<state>>` —
|
||||
status never moves by edit — `link create --out <blocker> --in <blocked> --type "Blocks"`,
|
||||
`comment create`. No parent: omit --parent — the item sits loose in the project. Custom fields go through --from-json; when dropped shares a status with done,
|
||||
set the resolution the same way so the two stay distinct.
|
||||
Epics sit loose in the project while [tickets.types].initiative is empty. Set that type only when the
|
||||
project has a level above Epic; then epics take it as --parent.
|
||||
"""
|
||||
|
||||
query = """
|
||||
One work item: `acli jira workitem view <key> --json --fields
|
||||
key,issuetype,summary,status,assignee,parent,labels,issuelinks,resolution,description`. after is the
|
||||
"is blocked by" links; Done with resolution Won't Do is dropped; the description's covers holds the source requirement ids. Children:
|
||||
`search --jql "parent = <key>"`. Words: `search --jql 'project = <key> AND text ~ "<words>"'`,
|
||||
narrowed by issuetype or status per the maps.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type.
|
||||
[tickets.types]
|
||||
initiative = "" # empty: the initiative stays local. Jira's default hierarchy is Epic > Story > Subtask; a level above Epic needs Plans (Premium or Enterprise). Set to "Initiative" only when the project has that level.
|
||||
epic = "Epic"
|
||||
story = "Story"
|
||||
spike = "Task" # plus label spike
|
||||
bug = "Bug"
|
||||
|
||||
# BMad state -> this store's status.
|
||||
[tickets.status]
|
||||
backlog = "To Do"
|
||||
in-progress = "In Progress"
|
||||
review = "In Review" # not a default Jira status; `setup` asks for it. With no review status, set it to the in-progress one
|
||||
done = "Done"
|
||||
dropped = "Done" # Jira default has no separate status; resolution Won't Do distinguishes it
|
||||
@@ -0,0 +1,75 @@
|
||||
# Store starter: Linear. `scripts/read_store.py` reads this file as the base layer; the project's
|
||||
# _bmad/custom/ticketing-store-config.toml names the store, fills the empty fields below, and replaces
|
||||
# any other key here, key by key.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# One tool: Linear's official MCP server. Initiatives hold projects, parent issues are epics, labels carry
|
||||
# type, relations carry blocking. Linear ships no CLI; community CLIs are not used here.
|
||||
|
||||
description = "Linear through its MCP server: projects, parent issues, relations, labels. For product teams on Linear."
|
||||
|
||||
[tickets]
|
||||
store = "linear"
|
||||
key = "" # Linear team key, e.g. "SHOP"; Linear assigns the number
|
||||
team = "" # the default team, key or name. A container's own `key` overrides it for everything under it
|
||||
access = """
|
||||
Use the Linear MCP (create_issue, update_issue, create_project, update_project, get_issue,
|
||||
list_issues, list_issue_statuses, create_issue_label, create_comment) against `team`. Confirm from the tool schema whether parent
|
||||
and blockedBy take identifiers (SHOP-12) or ids, and the exact tool names against the server's list at first use.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
severity is Linear's priority, set on the issue: P0 = Urgent, P1 = High, P2 = Medium, P3 = Low.
|
||||
risk is a label risk:<value>; `setup` creates the three. estimate is Linear's estimate field on a story; a label est:<value> on an epic. Empty field: no priority, no label.
|
||||
"""
|
||||
|
||||
reference = """
|
||||
Also pass the url in the issue's links so it shows in the sidebar.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Connect, unless the Linear tools already answer: add the MCP server https://mcp.linear.app/mcp to
|
||||
the agent tool in use (in Claude Code, `claude mcp add --transport http linear-server https://mcp.linear.app/mcp`),
|
||||
then sign in over OAuth. Do not use the deprecated /sse endpoint.
|
||||
`create_issue_label` for every non-empty value in [tickets.types], `hitl`, and risk:low, risk:medium,
|
||||
risk:high that the team lacks. States
|
||||
cannot be created from the MCP: `list_issue_statuses`, compare with [tickets.status], and tell the
|
||||
user what to add under Settings, Teams, Issue statuses, or which map entry to set in the project's
|
||||
store config. Linear's defaults (Backlog, Todo, In Progress, Done, Canceled) need no setup; [tickets.status]
|
||||
also expects an In Review state in the Started group.
|
||||
"""
|
||||
|
||||
write = """
|
||||
The team is the ticket's own `key`, else that of the nearest container above it, else `team` here.
|
||||
tracker_id is the issue's identifier and remote its url.
|
||||
`create_issue` creates, `update_issue` changes — title, description, state, assignee, parent,
|
||||
blockedBy, labels, priority, project. The issue's state is [tickets.status].<state>. Labels carry type, hitl, and
|
||||
risk:<value>; priority carries severity per `fields`. A standalone ticket: omit parent and project.
|
||||
Epics are parent issues. Set [tickets.types].initiative to map onto a Linear Initiative only when the
|
||||
server exposes initiative tools.
|
||||
"""
|
||||
|
||||
query = """
|
||||
One issue: `get_issue` with includeRelations — labels, priority, state, assignee, parent,
|
||||
project, relations, description; after is the "blocked by" relations, and the description's
|
||||
covers holds the source requirement ids. Many: `list_issues` by project, parent, label, state, or
|
||||
query words.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type.
|
||||
[tickets.types]
|
||||
initiative = "" # empty: the initiative stays local. Linear has no Epic object and Projects cannot nest; set to "initiative" only to map onto Linear Initiatives, which nest five deep and allow several parents, so the tree cannot round-trip.
|
||||
epic = "epic" # label on a parent issue
|
||||
story = "story"
|
||||
spike = "spike"
|
||||
bug = "bug"
|
||||
|
||||
# BMad state -> this store's status.
|
||||
[tickets.status]
|
||||
backlog = "Backlog" # or "Todo" — the team's choice
|
||||
in-progress = "In Progress"
|
||||
review = "In Review" # not a default Linear state; `setup` asks for it. With no review state, set it to the in-progress one
|
||||
done = "Done"
|
||||
dropped = "Canceled"
|
||||
@@ -0,0 +1,78 @@
|
||||
# Store starter: Notion. `scripts/read_store.py` reads this file as the base layer; the project's
|
||||
# _bmad/custom/ticketing-store-config.toml names the store, fills the empty fields below, and replaces
|
||||
# any other key here, key by key.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# One tool: Notion's official MCP server. One database holds every ticket; properties carry type,
|
||||
# status, assignee, and hitl; two self-relations carry hierarchy (Parent) and blocking (Blocked by);
|
||||
# a board view grouped by Status is the kanban. `setup` creates all of it.
|
||||
|
||||
description = "Notion through its MCP server: one tickets database with a board view, relations for hierarchy and blocking. For teams that plan in Notion and want tickets beside their docs."
|
||||
|
||||
[tickets]
|
||||
store = "notion"
|
||||
key = "" # prefix of the database's ID property, e.g. "SHOP" gives SHOP-12; empty = page ids only
|
||||
database = "" # url or id of the tickets database; empty = `setup` creates it
|
||||
access = """
|
||||
Use the Notion MCP (notion-fetch, notion-create-pages, notion-update-page,
|
||||
notion-query-data-sources, notion-create-comment, notion-get-users) against `database`. Confirm
|
||||
from the tool schema how relation and person properties are passed; `notion-get-users` resolves a name.
|
||||
An empty `database` means `setup` has not run.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
risk and severity are select properties: Risk (low, medium, high) and Severity (P0-P3); `setup`
|
||||
creates them, publish and update set them. estimate is a text property Estimate, created with the rest. Empty field: property left empty.
|
||||
"""
|
||||
|
||||
reference = """
|
||||
Notion renders urls as links; a Notion page url becomes a mention.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Connect, unless the Notion tools already answer: add the MCP server https://mcp.notion.com/mcp to
|
||||
the agent tool in use (in Claude Code, `claude mcp add --transport http notion https://mcp.notion.com/mcp`),
|
||||
then sign in over OAuth. Either way, share the database, or the page it will be created under, with
|
||||
the connection.
|
||||
When `database` is empty: ask which page to create under, then `notion-create-database` named
|
||||
Tickets with properties Title (title), Type (select, options per [tickets.types]), Status (status,
|
||||
options per [tickets.status]), Parent (relation to this database), Blocked by (relation to this
|
||||
database), Assignee (person), Covers (text), Risk (select: low, medium, high), Severity (select:
|
||||
P0, P1, P2, P3), Estimate (text), hitl (checkbox), and, when `key` is set, ID (unique id
|
||||
with that prefix). Then `notion-create-view`: a board grouped by Status. Give the user the url and
|
||||
put it in `database` in the project's store config.
|
||||
When `database` is set: `notion-fetch` it and compare its properties and options with the maps;
|
||||
add a missing option with `notion-update-data-source`, and tell the user about anything the
|
||||
schema needs a person to change.
|
||||
"""
|
||||
|
||||
write = """
|
||||
tracker_id is the ID property when `key` is set, else the page id; remote is the page url.
|
||||
`notion-create-pages` into `database` creates; `notion-update-page` changes any property or the
|
||||
content — Status, Assignee, Parent, Blocked by, Covers, Risk, Severity, hitl. Status is [tickets.status].<state>. No parent: leave the Parent relation empty. An initiative
|
||||
is a page of Type initiative with no Parent. The body is the page content.
|
||||
"""
|
||||
|
||||
query = """
|
||||
One page: `notion-fetch` — Title, Type, Status, Assignee, Parent, Blocked by, Covers, Risk,
|
||||
Severity, hitl, and the content; after is Blocked by. Many: `notion-query-data-sources` on `database` filtered by any
|
||||
property; `notion-search` scoped to `database` for a looser match.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type (Type select options; `setup` creates them).
|
||||
[tickets.types]
|
||||
initiative = "initiative"
|
||||
epic = "epic"
|
||||
story = "story"
|
||||
spike = "spike"
|
||||
bug = "bug"
|
||||
|
||||
# BMad state -> this store's status (Status property options; `setup` creates them).
|
||||
[tickets.status]
|
||||
backlog = "Backlog"
|
||||
in-progress = "In Progress"
|
||||
review = "Review"
|
||||
done = "Done"
|
||||
dropped = "Dropped"
|
||||
@@ -0,0 +1,57 @@
|
||||
# Store starter: tickets as files in the repo, the default. `scripts/read_store.py` reads this file as the
|
||||
# base layer; the project's _bmad/custom/ticketing-store-config.toml names the store and replaces any key
|
||||
# here, key by key. With no project file this starter is the whole config.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# Tickets and artifacts live together as markdown files under `output_folder` (layout in SKILL.md,
|
||||
# The ticket tree), committed in the monorepo or in a repo of their own. Status and assignee change by commit, and the skill never pushes,
|
||||
# so others see them only after a push and pull.
|
||||
|
||||
description = "Quick setup, and the default when nothing else is chosen. Tickets are markdown files under version control: in the monorepo, or in their own repo (recommended for mono and poly repos alike). No tracker or account needed. The simplest store — right for a solo developer or a very small team with no other tracking."
|
||||
|
||||
[tickets]
|
||||
store = "repo"
|
||||
|
||||
access = """
|
||||
Read and write the files under `output_folder`.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
risk and severity live in the frontmatter (risk: low|medium|high on any ticket; severity: P0-P3
|
||||
on bugs); estimate the same way. Nothing else carries them.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Tickets live in `output_folder` beside the documents. To keep them outside the project, set `output_folder`
|
||||
under `[core]` in `_bmad/custom/config.toml` to a folder beside it — the session then launches one level
|
||||
up — or in the workspace that already holds the repos. `git init` a new folder so tickets have
|
||||
history; skip that when the user picks a folder already under git, the monorepo included. Then
|
||||
create `backlog/` and the active initiative's folder there when they do not exist.
|
||||
"""
|
||||
|
||||
write = """
|
||||
Every change is an edit to the ticket's file, its `tickets.toml` entry, or, for a leaf's status and assignee, its plan through `tickets.py mark`.
|
||||
Commit only the files you wrote on the current branch. Root not in a git repo: skip the commit and say so.
|
||||
"""
|
||||
|
||||
query = """
|
||||
The files are the store: `tickets.py find`, `next`, and `status` answer from them. By a frontmatter field: `grep -rl "^type: bug" <folder>`.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type.
|
||||
[tickets.types]
|
||||
initiative = "initiative"
|
||||
epic = "epic"
|
||||
story = "story"
|
||||
spike = "spike"
|
||||
bug = "bug"
|
||||
|
||||
# BMad state -> this store's status.
|
||||
[tickets.status]
|
||||
backlog = "backlog"
|
||||
in-progress = "in-progress"
|
||||
review = "review"
|
||||
done = "done"
|
||||
dropped = "dropped"
|
||||
@@ -0,0 +1,76 @@
|
||||
# Store starter: Trello. `scripts/read_store.py` reads this file as the base layer; the project's
|
||||
# _bmad/custom/ticketing-store-config.toml names the store, fills the empty fields below, and replaces
|
||||
# any other key here, key by key.
|
||||
# It updates with the skill and holds only what is specific to this store: the rules every store shares
|
||||
# are in references/board.md. Comments and `description` are never loaded; `description` is for choosing a store.
|
||||
#
|
||||
# One tool: the official Trello MCP server. Trello is flat: lists carry status, labels carry type, card
|
||||
# urls in descriptions carry hierarchy and blocking. Using the defaults below: the board needs the lists
|
||||
# Backlog, In Progress, Review, Done, Initiatives, Epics and a label per type plus hitl; `setup` says how. Members, comments, attachments, and label creation
|
||||
# are not in the MCP yet. Weakest fit of the shipped configs; expect to tune it.
|
||||
|
||||
description = "Trello through its MCP server: lists as status, labels as type, card links as hierarchy. For small teams that run a Trello board and want to keep it."
|
||||
|
||||
[tickets]
|
||||
store = "trello"
|
||||
key = "" # ids are card short links (from the card url); leave empty
|
||||
board = "" # the board url
|
||||
access = """
|
||||
Use the Trello MCP (trelloReadBoard, trelloReadCard, trelloWriteCard, trelloWriteChecklist,
|
||||
trelloSearch) against `board`. Write tools take the server's ids, not urls: resolve a url with
|
||||
`trelloReadCard get` first.
|
||||
"""
|
||||
|
||||
fields = """
|
||||
risk is a label risk:<value>; severity a label P0-P3. The MCP cannot create labels: `setup` asks
|
||||
the user to add them with the type labels. estimate is an `estimate:` line in the description. Empty field: no label.
|
||||
"""
|
||||
|
||||
reference = """
|
||||
Attachments are not in the MCP yet; the description line is the reference.
|
||||
"""
|
||||
|
||||
[verbs]
|
||||
setup = """
|
||||
Connect, unless the Trello tools already answer: add the MCP server https://mcp.trello.com/v1 to
|
||||
the agent tool in use (in Claude Code, `claude mcp add --transport http trello https://mcp.trello.com/v1`),
|
||||
then sign in over OAuth with write permission.
|
||||
The MCP can create a board but not lists or labels. When `board` is empty, offer to create one;
|
||||
then ask the user to add the lists in [tickets.status] (Backlog, In Progress, Review, Done) plus
|
||||
Initiatives and Epics, and labels for every value in [tickets.types] plus hitl, risk:low, risk:medium, risk:high, and
|
||||
P0-P3. Check with
|
||||
`trelloReadBoard` before first publish and name what is still missing.
|
||||
"""
|
||||
|
||||
write = """
|
||||
tracker_id is the card's short link and remote its url.
|
||||
`trelloWriteCard create` makes a card, `update` changes description or labels, `move` changes
|
||||
list, `archive` is dropped. Status is the list [tickets.status].<state>; initiative cards live in the Initiatives list,
|
||||
epics in Epics, both carrying a `status:` line at the top of the description. Labels carry type,
|
||||
hitl, risk, severity. Trello has no fields for the rest — conventions in the description:
|
||||
`parent: <card url>` as its first line, an `assignee:` line (no member tools yet), after as
|
||||
card urls; the parent card keeps a checklist with each child's url. A standalone ticket: no parent line.
|
||||
The body is the description, and the `assignee:` line stays in it.
|
||||
"""
|
||||
|
||||
query = """
|
||||
One card: `trelloReadCard get` by url — name, list, labels, description (assignee, parent,
|
||||
covers, after live there), checklists; a container's children are its checklist items. Many:
|
||||
`trelloSearch` scoped to `board`, or `trelloReadBoard` filtered by the maps.
|
||||
"""
|
||||
|
||||
# BMad type -> this store's type.
|
||||
[tickets.types]
|
||||
initiative = "initiative"
|
||||
epic = "epic"
|
||||
story = "story"
|
||||
spike = "spike"
|
||||
bug = "bug"
|
||||
|
||||
# BMad state -> this store's status.
|
||||
[tickets.status]
|
||||
backlog = "Backlog"
|
||||
in-progress = "In Progress"
|
||||
review = "Review"
|
||||
done = "Done"
|
||||
dropped = "archived"
|
||||
@@ -0,0 +1,50 @@
|
||||
# DO NOT EDIT -- overwritten on every update.
|
||||
#
|
||||
# Workflow customization surface for bmad-ticket.
|
||||
# Team overrides: {project-root}/_bmad/custom/bmad-ticket.toml
|
||||
# Personal overrides: {project-root}/_bmad/custom/bmad-ticket.user.toml
|
||||
|
||||
[workflow]
|
||||
|
||||
# --- Universal defaults. Merge: scalars override, arrays append. ---
|
||||
activation_steps_prepend = []
|
||||
activation_steps_append = []
|
||||
persistent_facts = [] # `file:` entries are paths or globs under {project-root}; anything else is a fact verbatim
|
||||
on_complete = ""
|
||||
|
||||
# The keys below are settings a team picks. The method — how an initiative and an epic are sliced, what a
|
||||
# good acceptance criterion is, how risk is scored, what the checks are — is in references/ and is not a
|
||||
# key. A team rule on top of the method goes in `persistent_facts`, or in the project's AGENTS.md when the
|
||||
# build skills need it too (the risk definition, the criteria rule).
|
||||
|
||||
# When tickets publish to a tracker; on the repo store, committing the approved tickets.toml is the publish. at_inception: the whole
|
||||
# agreed breakdown at inception, so the team sees the plan there; on_start: each ticket when it starts. The user can override.
|
||||
publication = "at_inception" # at_inception | on_start
|
||||
|
||||
# How this team draws epic boundaries. Empty: the rule in references/slice.md. Set, it replaces that rule:
|
||||
# the team's own text, or a pointer to where its boundaries are recorded — a url, AGENTS.md, a team map,
|
||||
# anything the agent can read.
|
||||
slice_to_epics = ""
|
||||
|
||||
# The team's own validation checks, one array per scope, run with the skill's in references/validate-checks.md.
|
||||
# Write `checks.<scope> = [...]`; an override that sets `checks` as one string replaces the whole table.
|
||||
checks.ticket = [] # one ticket
|
||||
checks.set = [] # every entry in an epic's breakdown
|
||||
checks.tree = [] # the initiative and its epic envelopes
|
||||
checks.dependencies = [] # missing prerequisites, run with the set and with the tree
|
||||
checks.closure = [] # a container before it closes
|
||||
|
||||
# Ticket templates, one per type. They apply to tickets the agent writes; `tickets.py pull` writes a fixed layout,
|
||||
# so a pulled story does not follow `story_template`.
|
||||
# Swap a template to change the criteria format (the shipped one is numbered Given/When/Then; a table, a checklist)
|
||||
# or to add your own sections. A container's envelope fields and its number of Done when checks are the container
|
||||
# templates' too.
|
||||
# Keep a home, under any name, for each section references/ticket.md refers to: Description,
|
||||
# Acceptance Criteria, Boundaries (stories), References, and Notes; for containers also Outcome,
|
||||
# Requirements, and Done when; Reproduction and Cause Hypothesis for bugs, Approach for spikes. `Decision:` and `Dropped:` lines live in Notes.
|
||||
# Spikes use "Verify:" lines instead of Given/When/Then, and a pulled story carries one Verify: line unless its entry has `refine = true`; both deliberate.
|
||||
initiative_template = "{skill-root}/assets/initiative-template.md"
|
||||
epic_template = "{skill-root}/assets/epic-template.md"
|
||||
story_template = "{skill-root}/assets/story-template.md"
|
||||
spike_template = "{skill-root}/assets/spike-template.md"
|
||||
bug_template = "{skill-root}/assets/bug-template.md"
|
||||
@@ -0,0 +1,80 @@
|
||||
# Board
|
||||
|
||||
Operations on existing tickets. Read an existing ticket before changing it; if it has been published to a tracker, query its current remote state too.
|
||||
|
||||
`tickets.py` below is `uv run {skill-root}/scripts/tickets.py --project-root {project-root}`. `tickets.py <command> --help` gives a command's arguments and output keys.
|
||||
|
||||
## Reading the tree
|
||||
|
||||
- A ticket the user names — "refine 1.2", "start ABCD-13", "build the cart service scaffold" — resolves through `tickets.py find [<folder>] <ref>` before you pull, review, refine, start, or mark it. `<ref>` is `<epic id>.<entry id>`, an entry id inside the epic folder, a tracker id, a file name, or an unbroken phrase from the title. When the user's words are looser than that, read `status` and pick the ticket yourself, asking when more than one fits. In the result, `story_file` is null until the entry is pulled, and `plan` is where the plan is or goes.
|
||||
- Progress lives on tickets, not in a separate sprint or status file. `tickets.py next [<folder>]` groups the open tickets by what can happen next; `tickets.py status [<folder>]` reports every ticket with its `status`, `tracker_status`, and `state`, what it blocks, and counts by state.
|
||||
- `<folder>` is an epic (its folder name is enough), `backlog`, or the initiative for all its epics at once. Left out, it is the active initiative: `next` and `status` add `backlog`, the same view of `backlog/`, so show both, and `find`, `mark`, and `mirror` look in `backlog/` for a ticket the initiative does not hold. A done standalone ticket stays in `backlog/` beside its plan.
|
||||
- With a tracker, bring the tree up to date first (Tracker sync, below) and pass `--synced` to `next`.
|
||||
|
||||
When one of these comes back with something in it, act on it:
|
||||
|
||||
| Key | Means | Do |
|
||||
|---|---|---|
|
||||
| `unpinned_after` | an epic has tickets, but none waits on the epic its `after` names | with the user, add the prerequisite to the entry that needs it |
|
||||
| `undeclared_after` | an entry waits on an epic its own epic does not declare | with the user, add that epic to this epic's `after` in the initiative's breakdown with its `needs`, or drop the prerequisite |
|
||||
| `order_conflict` | an epic waits on one later in build order | reorder or merge the epics, or record why in a `Decision:` line |
|
||||
| `drift`, on a row | the leaf file's `after` or `hitl` differs from the entry's; the row gives both values | show them to the user and make them equal |
|
||||
| `problems` | a plan whose `ticket` names nothing, or whose `status` is unknown, so its ticket reads as blocked | show each to the user and fix the plan; `mark` sets a valid status |
|
||||
| `waiting_on`, on a `blocked` row of `next` | the prerequisites not yet done or in review | say what the ticket waits on |
|
||||
| `unknown`, on a `blocked` row of `next` | a question that must be settled before the ticket starts | settle it with the user, remove it, and record the answer as a dated `Decision:` line: in the file's Notes, or in its epic's Notes when it has no file |
|
||||
|
||||
## Publish
|
||||
|
||||
Approving a breakdown records it in the epic's `tickets.toml`; it does not start work. On the repo store, committing the approved `tickets.toml` is the publish. On a tracker, `{workflow.publication}` says when: `at_inception` offers to publish the agreed set after validation; `on_start` publishes each ticket when it starts. An explicit user request takes precedence.
|
||||
|
||||
Publishing is not a status: on a tracker it is the remote item existing, with `tracker_id` and `remote` in the leaf file. Publish through `write`:
|
||||
|
||||
- The leaf's file is written first, with `tickets.py pull` when it has none, because the file is the body the tracker receives: the whole file without the frontmatter lines `tracker_id`, `remote`, `tracker_status`, `status`, and `assignee`.
|
||||
- Create parents before children, and a prerequisite before what waits on it. A prerequisite that is still local joins the proposed publication scope. An `after` naming a sibling's id or `<epic id>.<entry id>` is that entry's remote item; `epic-<slug>` is the epic's item.
|
||||
- A type mapped to `""` is never sent: it stays local and its children attach to the nearest published ancestor, which is how an initiative behaves on a tracker whose hierarchy has no level above the epic. Say so once when it first matters rather than at every publish.
|
||||
- At first publish, or when the tracker lacks something the maps or `fields` name — a label, list, status, or property — offer `setup`.
|
||||
- Then mirror what the tracker returned (Tracker sync). `mirror` writes leaves only: a published container's `tracker_id` and `remote` go into its own file by edit.
|
||||
|
||||
An unrefined ticket may be published for planning visibility. One that needs refining keeps `refined: false`, and that line stays in the published body; the `backlog` state means available to start. Refinement updates that same file and remote item, preserving identity.
|
||||
|
||||
## Start
|
||||
|
||||
Candidates are `next`'s `ready_to_start` rows: tickets, with or without a file, that are not started, blocked, or waiting to be refined, whose prerequisites are done or in review, whose `gated_by` epics are done, and that carry no `unknown`. Offer the first, in build order; when work can run in parallel, offer every one that is unassigned.
|
||||
|
||||
Before starting a candidate, read it and its source, passing its row's `ref` to `find`. One in `ready_to_refine` is refined first per `ticket.md`. Confirm it is in `ready_to_start` and not assigned to someone else. Then, with the user's approval, publish it if it is not yet published, and start it. On a tracker, `write` transitions the item to `[tickets.status].in-progress` and the mirror sets `tracker_status: in-progress`. On either store the ticket then goes to the build, `bmad-build` or `bmad-build-auto`, which writes `status` in the ticket's plan as it works; on the repo store that is all starting is. An entry's `plan_checkpoint` waits for a person to approve the ticket, or the builder's plan when it is not refined, and `done_checkpoint` waits after the ticket closes. Both are on the ticket's row, and the orchestrator that dispatches an unattended run does the waiting: `bmad-build-auto` reads neither, so run without an orchestrator nothing pauses.
|
||||
|
||||
## Status
|
||||
|
||||
A leaf's `status`, `blocked_at`, and `blocked_reason` live in its plan, `<type>-<slug>-plan.md` beside where its file is or would be, joined by the plan's `ticket` field; a leaf file's own `status` is read only when there is no plan. Who writes each status:
|
||||
|
||||
| Status | Written by |
|
||||
|---|---|
|
||||
| `draft`, `ready-for-dev`, `in-progress`, `in-review`, `built` | `bmad-build`, `bmad-build-auto` as they work |
|
||||
| `blocked` | `bmad-build-auto` when it halts, with the reason in the plan |
|
||||
| `done` | the user, or an orchestrator, through `tickets.py mark` |
|
||||
| `dropped` | `bmad-ticket`, when the user says so |
|
||||
|
||||
A ticket's state is `planned` for an entry with no file and no plan; else `tracker_status` when present; else from `status`: none, `draft`, or `ready-for-dev` is `backlog`; `in-progress` or `blocked` is `in-progress`; `in-review` or `built` is `review`; `done` and `dropped` are themselves. `tracker_status` exists only on a tracker store: the BMad word for the tracker's current status.
|
||||
|
||||
No skill moves a ticket past `built`, which means the build finished and nobody has called it done. `bmad-ticket` never moves a leaf on its own: it changes a leaf's status or assignee only when the user asks, for `done`, `dropped`, or a person working the ticket by hand. When the user says a ticket is done, mark it for them.
|
||||
|
||||
- **Repo store.** `tickets.py mark [<folder>] <ref> <status> [--assignee <who>] [--blocked <reason>]`, then the commit the `write` verb describes. `mark` takes its folder and ref as `find` does, writes the plan and never the leaf file, and creates a plan holding only frontmatter when there is none. It writes what it is told; the checks in this file are yours. The assignee is in the plan.
|
||||
- **Tracker.** `write` transitions the item to `[tickets.status].<state>`, the ticket's `state`, and sets its assignee; it never sends `status` as a word. Then mirror the item (Tracker sync): its status comes back as `tracker_status` and its assignee lands in the leaf file. `mark` refuses a tracker store.
|
||||
- **Waiting on a person or an answer**, not on a prerequisite, on the repo store: `mark` with the ticket's current status, `draft` when it has none, and `--blocked <reason>` sets `blocked_at` (today) and `blocked_reason`, and `next` lists the ticket under `blocked`; a `mark` without it clears both when the ticket moves.
|
||||
- **A container's** `status` is `bmad-ticket`'s, an edit to its file: absent until the first ticket under it starts, then `in-progress`, and `done` or `dropped` when the user confirms it; containers never take review. A published container gets the same transition through `write`.
|
||||
|
||||
## Close, drop, move
|
||||
|
||||
- Closing every child does not close the parent. Run the closure check in `validate.md` against its requirements and Done when; the user confirms the parent is complete.
|
||||
- Drop only after a `Dropped:` line says why: in the ticket's Notes, or in its epic's Notes when the ticket has no file. Then set its status to `dropped` as Status says; an entry never published to the tracker takes `tracker_status: dropped` through `mirror`. An entry is never deleted from an approved `tickets.toml`, so its id is never reused. A dropped ticket still blocks what waits on it, in any epic: `status` lists them under `blocks`; remove or repoint it in each one's `after` with the user. Cancelling a container cancels its descendants after the user confirms.
|
||||
- Moving a leaf to another parent: first write the old folder's `next_id` (from `tickets.py status <folder>`) at the top of its `tickets.toml` as `next_id = <n>`, so the id that leaves is not given out again. Then move its entry from the old `tickets.toml` to its place in the new one's build order, with that folder's `next_id` (from `tickets.py status <folder>`) as its id; move the leaf file and its plan into the new folder; set the file's `id` and `parent` and the plan's `ticket` to the new id. Then rewrite both directions of `after`: its own, where a sibling it left is now `<epic id>.<entry id>`, and every `after` that named it. Move its `covers` ids between the two epics when the requirement moves with it. An epic moves as its whole folder, with its `[[epic]]` table. Then run `tickets.py status` on the initiative and clear what it reports.
|
||||
|
||||
## Tracker sync
|
||||
|
||||
The tree is the working copy and a tracker is its remote; nothing syncs on its own. After every `write`, and after the `query` that precedes a board view, put what the tracker holds into the tree with `tickets.py mirror [<folder>]`. It reads a JSON array on stdin, one object per ticket: `ref` (or the `tracker_id` the ticket already carries) and any of `tracker_id`, `remote`, `tracker_status`, `assignee`, and `after`. `tracker_status` is the BMad state the tracker's status maps back to through `[tickets.status]`. After a `write`, it is the state that was sent. After a `query`, when two states share one tracker status and `query` does not say how to tell them apart, keep the ticket's current `tracker_status` if it is one of them, else take the earlier state. `assignee` is the tracker's name for the person; `after` lists the prerequisites as tracker ids, each a string, since a bare number there is a sibling's entry id. `mirror` pulls a leaf file when the entry has none, writes only the keys given, removes a line whose value is an empty string, and never writes `status`, which is the build's.
|
||||
|
||||
- A leaf the tracker holds and the tree does not comes back under `unmatched`, also when the mirror failed because a known ticket's `after` names it: add its `[[entry]]` to the parent's `tickets.toml` with the folder's `next_id` and the item's type, title, and a one-sentence description, mirror it again with `ref` naming the new entry, then put the item's body in the pulled file.
|
||||
- A published container is queried with its leaves, and `mirror` does not write containers. When the tracker's status for one differs from its file's, tell the user; closing it here still takes the closure check.
|
||||
- A body that differs from the file: show both and ask.
|
||||
- A ticket the tracker shows under another parent is moved as Close, drop, move says, then mirrored again.
|
||||
- Commit the changed files on the current branch when `output_folder` is in a git repo; never push.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Slicing
|
||||
|
||||
An initiative is sliced into epics: containers the product owner and developer own and complete. Inception plans the whole selected epic so AI agents can build it. The user's preference for a split comes first.
|
||||
|
||||
When an initiative has epics, every story is under an epic. Work that fits one epic is proposed as one epic; say so. Never offer an initiative without epics: it happens only at the user's request, is planned like an epic with stories directly underneath, and you say once that the initiative folder fills with ticket files.
|
||||
|
||||
## Every container, in order
|
||||
|
||||
Apply this when authoring the initiative or incepting the selected epic.
|
||||
|
||||
1. **Envelope.** A document in the folder is an input, not the container. When the container file is missing, offer to create it from its template: title, a paragraph of intent, Outcome, Done when, boundaries, known decisions, and references. An epic also records its parent and the parent requirement ids it owns in `covers`. Done when is three to six checks at the altitude of the source's ids; it is the definition of the container and is never deferred.
|
||||
2. **The requirement source at this altitude.** The template's Requirements placeholder says what the section holds. A numbered spec anywhere in the container's folder is the source instead. Offer `bmad-spec` only when the source outgrows the section or the user asks, and pass it the container's folder: its `spec-<slug>/` goes inside that folder, owns the ids, and `covers` still maps them upward. The architecture spine, UX design, and research inform everything below either way.
|
||||
3. **Complete the container** per `ticket.md`: the requirement source, constraints as References, known unknowns in Notes, and a re-read of Outcome and Done when against the source. The product owner and developer agree on its scope: propose the fields together with reasons, discuss what is unsettled rather than repeating decisions already made, and confirm before slicing.
|
||||
|
||||
## Learn the codebase and team first
|
||||
|
||||
Start from the parent chain: the parent ticket, its spec, and what they reference, including the design when the work is user-facing. Then the codebase: greenfield or brownfield; mono or poly repo; team, service, and UI boundaries; vocabulary and recorded decisions — from the architecture document, else the repo layout. Then the areas the work will touch, enough to see which stories would change the same code. Tell the user what you read and concluded; ask what is wrong or missing and whether there are other references or tools you do not already know of.
|
||||
|
||||
Where the source contradicts the code or another source, add `Source conflict: <id or section> — <what the source says> vs <what was found>` to the Notes of the container whose source it is, and tell the user. When the source is a BMad spec, offer to pass the correction to `bmad-spec`.
|
||||
|
||||
## Ask the questions that decide the split
|
||||
|
||||
Use what is already known. Ask the remaining questions that change the split: what is first worth demoing; what is least certain; what will the first piece teach about the rest; whether the user has a split in mind; how the team defines epics. Group related questions and say which answer you would pick and why.
|
||||
|
||||
When the user states how their team cuts epics, offer to save it as `slice_to_epics` under `[workflow]` in `{project-root}/_bmad/custom/bmad-ticket.toml`.
|
||||
|
||||
Record each choice the user makes while slicing — a boundary, the tracer bullet, the sequencing, deferred scope, a declined suggestion — as a dated `Decision:` line in the container's Notes when it is made, not after approval.
|
||||
|
||||
## Initiative into epics
|
||||
|
||||
Existing epics are the working set: read them first and revise them in place, or drop one per `board.md` when it no longer fits; add only after the user confirms the set is insufficient. Draw the boundaries by the team's own rule, `{workflow.slice_to_epics}`, when it is set. Otherwise:
|
||||
|
||||
- An epic is one capability from the source, or a tightly coupled pair, delivered to production by one owner: a dev or pair with agent lanes.
|
||||
- Propose epics along the source's capabilities. Merge two when one owner and one module deliver both. Two epics need at most a contract between them; say what each needs from the ones before it.
|
||||
- A unit (module, service, or bounded context) is an epic boundary when it is also the ownership or deployment boundary and its Done when still reads as something a product owner can check. A module that is only a code folder is not.
|
||||
- An epic whose boundary names more than one outcome or owner is two epics.
|
||||
|
||||
Under either rule:
|
||||
|
||||
- No boundary applies: one epic. Split only for a distinct outcome, owner, or a part the user wants usable early, never for a ticket count.
|
||||
- A unit the work only consumes or configures gets no epic: it is a touch point, named in the initiative's Boundaries with the epic that owns the work there.
|
||||
- The platform baseline (scaffold, environments, CI, deployment, operations) is the opening epic, the first `[[epic]]` in the initiative's breakdown, or the first stories of the first epic.
|
||||
- Every epic delivers to production; its Done when includes the integrated verification for what it delivers.
|
||||
|
||||
Draft the set, run the tree check in `validate.md` on the draft, then present it in recommended build order and say in one sentence which rule cut it. For each epic: title, one to three sentences of what is true when it is done, the parent ids it owns, its boundary, and what it needs from the epics before it. With the set, present:
|
||||
|
||||
- the tracer path across epics, when the first demo cuts through several;
|
||||
- every touch point, with the epic that owns it;
|
||||
- the decisions more than one epic must adopt: a contract, a message or data format, a shared value list. One repo or one unit has none.
|
||||
|
||||
Work with the user on order, boundaries, merges, splits, and anything unplaced.
|
||||
|
||||
On confirmation, write the initiative's `tickets.toml` from `{skill-root}/assets/tickets-template.toml`: one `[[epic]]` per epic in build order, each with an `id`, `after` naming what it needs and from which epic; `after` on an epic file only for a whole-epic gate. Each new epic gets its folder and envelope with Outcome and Done when.
|
||||
|
||||
For the decisions more than one epic must adopt, offer `bmad-architecture` to settle them in the spine, and cite the spine section in each adopting epic's References. Declined: add each as a `story` entry (`hitl = true`) in the `tickets.toml` of the opening epic, and give each adopting epic `after = [{ epic = <opening>, needs = <the decision> }]`; its entries name that entry in `after` at their inception.
|
||||
|
||||
Stop at this level unless the user wants to incept an epic now; complete and plan only the selected epic. The others retain their scope, references, and place in the order without a breakdown.
|
||||
|
||||
## Epic into stories
|
||||
|
||||
The epic is ready to be worked and its spec exists or the user chose to go without. Read its breakdown, every child ticket, and their results before proposing changes. Plan the entire epic, not only its next story. Put each unknown that must be settled before implementation to the user: answer it now, record it as the `unknown` of the entries it affects, or add a spike when they ask for one. Never guess the design of work that depends on it.
|
||||
|
||||
What makes a good story:
|
||||
|
||||
- It is one implementation step toward the epic's Done when, small enough that one agent session, starting from the entry, its epic, and the source, plans and finishes it. It need not be user-visible on its own; the epic is the unit of value. Too small when setup outweighs the work: merge. An epic that is itself one session's work gets one story.
|
||||
- The first entry is the tracer bullet: the thinnest path through every layer the epic touches, proving they connect. Whatever setup that needs, including a starter the user runs, is a hitl step on that entry. For it, and for anything the user wants demoable, the entry says what someone can see running when it is done. After the tracer bullet comes the story the user is least sure of.
|
||||
- A lane is a run of stories that touch the same code, in order; stories in different lanes never touch the same code. Two that would are one story, or one waits on the other. A boundary two lanes share gets its interface and a stub as its own early story, so both lanes open at once.
|
||||
- Done is one runnable check. A story whose check needs another story's work goes after it.
|
||||
- Split, never shrink: "for now", "placeholder", "simplified", "wired later" is a second story.
|
||||
- Setup and each hitl step go on the first story that needs them, under the relevant epic and never loose under the initiative; initiative-wide setup belongs to the opening epic. Tests are part of every story; the one test story is the closing end-to-end suite below.
|
||||
- Eight to twelve stories is typical, not a limit. Fifteen can be right when they are one lane with one owner; six can be too many when two owners are inside. Past the typical range, say so and offer a split first; the user decides.
|
||||
|
||||
What opens and closes the breakdown:
|
||||
|
||||
- Offer an opening refactor when poor code quality or missing standards would make the epic's tickets hard.
|
||||
- An epic of more than three entries gets a closing story "Refactor sweep" that waits on every other entry except a closing end-to-end suite. Its scope is set when it starts, from the plans and the review findings deferred during the epic. It takes cleanup only; scope pushed out of another story is a new story. Propose it by default; when the user declines, record a `Decision:` line in the epic's Notes.
|
||||
- One closing end-to-end suite across the epic, after the sweep, is offered when the source has a test plan or the user wants one.
|
||||
|
||||
Draft one breakdown in build order, run the set check in `validate.md` on the draft, then present it, and adjust size, order, and prerequisites with the user until they approve the set. Each entry follows `{skill-root}/assets/tickets-template.toml` and has an `id` it keeps for good; the order of the tables is the build order. What the template does not say:
|
||||
|
||||
- `description` says what the entry delivers toward the ids in its `covers`, and `verify` how that result will be checked: one sentence each.
|
||||
- `after` holds real prerequisites only, never the sequence. For each `after` on this epic in the initiative's breakdown, put the provider in `after` of every entry that needs it: `<epic id>.<entry id>` when the providing entry exists, `epic-<slug>` until it does.
|
||||
- `refine = true` only where the user asks for full criteria on that entry; a bug gets them without it.
|
||||
- `references` holds only what the entry needs beyond the epic's own References: a spine section, a design screen, a document. `notes` holds only what the user said, in their words.
|
||||
- When the epic will run unattended, ask for `plan_checkpoint` and `done_checkpoint` per entry, proposing `plan_checkpoint` on a high-risk one.
|
||||
- Each touch point this epic owns is an entry or part of one.
|
||||
|
||||
With the draft, name the tracer bullet, what can run in parallel, and any deferred scope. With the approval question, ask once whether the user wants to change a description or add a note or reference to any entry.
|
||||
|
||||
A split at inception is a second epic folder and envelope, a new `[[epic]]` in the initiative's breakdown with its `after`, the covers ids moved, and the agreed entries placed under the right epic; no file is renamed.
|
||||
|
||||
On approval, write the whole set into the epic's `tickets.toml`. No leaf file is written until its entry is pulled.
|
||||
|
||||
Then run `uv run {skill-root}/scripts/tickets.py --project-root {project-root} status <initiative folder>`. In `epics`, this epic's `blocks` lists the tickets in other epics whose `after` names the whole epic; replace each with the entry that delivers what it waits for. Clear any `unpinned_after`, `undeclared_after`, or `order_conflict` it reports as `board.md` says. Publication follows `board.md`.
|
||||
|
||||
Then tell the user the entries are ready to build, and what that means for how they build. With `bmad-build`, each ticket is refined during the build: the builder questions the user and writes the criteria itself, so refining here repeats that work. With `bmad-build-auto`, a loop, or a factory, nobody answers questions during the build and the entry is all the builder gets: recommend one more review of the sequence and of each entry now (`ticket.md`), and offer the checkpoints where none are set.
|
||||
|
||||
## Re-slicing
|
||||
|
||||
When completed work changes the picture, revisit the whole remaining breakdown with the user. Update unstarted entries and tickets in place, preserving each `id`; published changes go through the store. Do not rewrite completed or active work as a new breakdown. Run the set check on the revised draft before the user approves it, and record the reason in the epic's Notes.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Setting up the store
|
||||
|
||||
The store config is two layers that `{skill-root}/scripts/read_store.py` reads as one. The base is the skill's starter for the store, `{skill-root}/config/<store>-ticketing.toml`, which updates with the skill. Over it goes the project's file, `{project-root}/_bmad/custom/ticketing-store-config.toml`: it names the store and holds only what this project sets, and a key it sets replaces the starter's. With no project file the store is the repo store, and nothing needs setting up.
|
||||
|
||||
Run this when the user wants a tracker connected or the store reconfigured or switched.
|
||||
|
||||
1. **Choose.** `uv run {skill-root}/scripts/read_store.py --starters` lists the shipped stores with a description of each. Every hosted one has a free tier; choosing it costs an account setup, or only the config when the user already has an account. The user may also bring a config of their own, as a file or a url.
|
||||
2. **Write the project file.** `[tickets]` with `store` and the address fields the starter leaves empty — key, repo, site, team, database, board — filled from the user's answers; the comments in the starter file say what each one is. Copy nothing else from the starter: a key the project file leaves out follows the skill as it updates. A project file that is a whole copy of a starter predates this layout: cut it down to `store`, the address fields, and what the team changed. A config the user brings goes into the project file whole; so does a store with no starter, modelled on the nearest one. For the repo store, delete the project file.
|
||||
3. **Connect.** Offer to run the store's `setup` verb: tool install, auth, and the labels, lists, statuses, or properties the maps name. Where the tracker's types or statuses differ from the starter's maps, set those entries under `[tickets.types]` or `[tickets.status]` in the project file. A state the tracker has no status for takes the status of the state before it, never an empty value: with no review status, `review` takes the in-progress one.
|
||||
4. **Prove it.** Offer a test. A store that already has items: `query` a few back — that proves auth, the maps, and the path back. An empty store: create one ticket in `backlog/` titled "BMad setup test — safe to delete" and prove `write` and `query`: create it, query it back, move it to `in-progress`, assign it, then drop it. Give the user the `remote` url so they can watch the item and its history.
|
||||
|
||||
A house rule on top of a verb, such as a label every new issue takes, goes in `persistent_facts` in this skill's customization. Set a whole verb under `[verbs]` in the project file only when the store's commands differ, because a replaced verb no longer follows the skill.
|
||||
|
||||
Then go on with what the user came to do. When that is done, offer once a `bmad-customize` pass on this skill: its `customize.toml` holds the team's epic boundary rule, publication timing, own checks, and templates.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Writing a ticket
|
||||
|
||||
A ticket starts from what is known: what exists when it is done, how it will be verified, what must not change, and what is already decided. Ask only what remains unsettled, offering a default for each, then draft into the ticket tree. Under an epic with a breakdown, the ticket is an entry, with the keys `{skill-root}/assets/tickets-template.toml` shows, placed where it goes in the build order; it gets a file now only when it carries full criteria, and otherwise `pull` writes one when it is reviewed or published. A standalone ticket is a file in `backlog/`. Either takes its `id` from `next_id` in `uv run {skill-root}/scripts/tickets.py --project-root {project-root} status <folder>`. Open only the template for the type: `{workflow.initiative_template}`, `{workflow.epic_template}`, `{workflow.story_template}`, `{workflow.spike_template}`, or `{workflow.bug_template}`. Its placeholders say what each section holds; its example is the level of detail to match, not copied.
|
||||
|
||||
## Each fact lives in one place
|
||||
|
||||
The input (PRD, brief, notes) owns the product argument. Reference a level's requirement source rather than duplicating it. The container owns Description, Outcome, Done when, Boundaries, References, Notes. An epic's `covers` records the parent requirement ids it owns, and every id it assigns locally — in Requirements or its own spec — maps to one of them. An initiative's `covers` records its source ids. Adding a local spec does not replace upstream coverage; update affected references and child mappings with it.
|
||||
|
||||
A story under an epic is one slice of its build order: `covers` cites ids from the epic's requirement source, and its description says what it delivers toward them. Several stories can cover one requirement. Where full criteria are written ("Reviewing or refining an existing ticket" says when), add criteria for what changes and for the failure paths, boundaries, and binding decisions; point at the source for the rest.
|
||||
|
||||
## Rules the template cannot carry
|
||||
|
||||
- Acceptance criteria, where they are written, in the template's format: each is a check the implementer can prove it met without pointing at code, and together they are enough that building the wrong thing cannot pass. Behavior that must stay as it is goes under Boundaries.
|
||||
- Every sentence is short and declarative, in common words and the project's own vocabulary: the reader has only the ticket. Say each thing once, with no invented term, filler, or metaphor. When the user's text misses this or the criteria rules, offer the rewrite with the reason and let the user decide; show what is missing, not only what is written.
|
||||
- A reference is a line in References, `type — location, section`: location is a path from `{project-root}`, or a url for a remote source, and is opened before it is cited. Name the nearest document, not the documents behind it. `reference` in the store's `tickets` table, read at activation, says what else that store needs; the repo store has none.
|
||||
- No source-code paths or snippets; the builder reads the repo. A snippet stays only when it is the decision itself, not an illustration of it. A path the user wants recorded goes in Notes.
|
||||
- A UI ticket links its design in References; criteria stay functional, layout lives in the design. No design and user-facing: offer `bmad-ux` first; declined, say the builder will guess the layout unless they add details in Notes.
|
||||
- `hitl: true` only when a person must do part of the work; say which step in the Description, and spell known steps out in the criteria or Notes.
|
||||
- Risk on every ticket: `low`, `medium`, or `high`, the business impact if this change is wrong, proposed with a one-line reason; the user's value wins. A high-risk ticket names one check outside its own criteria in Notes: a person who confirms, a suite beyond the ticket's tests, a monitor. The build confirms or raises the risk in its plan, which is where a CI process reads it to decide how much review a change gets.
|
||||
- Severity on a bug, as the bug template defines it. An `estimate` is recorded only when the user gives one: on the entry or in the ticket's frontmatter, carried to a tracker as the store's `fields` says. Never propose one.
|
||||
- A bug carries a reproduction and a cause hypothesis, never a fix. Missing steps: ask; unclear: tighten until someone else could follow them. Run them when cheap; if the behavior already holds, say so with evidence and create nothing. Criteria include tests for the condition found and fixed, and name the other valid outcome: proof no change is needed.
|
||||
- A spike names the question, who waits on the answer, and where it is recorded. A spike is `hitl`. When tickets in more than one epic wait on the answer, it is a decision several epics adopt: handle it per `slice.md`, not as a spike inside one of them.
|
||||
- Notes holds what is not in the repo or the source and what is unsettled, each line marked, and only what is local to this ticket. Anything touching more than one ticket lives in the parent's Notes and is referenced; an unknown that gates work goes to the user, and becomes a spike its dependents list in `after` only when they ask for one. `Assumption:` — offer each; confirmed, it becomes a dated decision; corrected, the ticket changes. `Open question:` — answering it is part of the ticket's work when it starts. `Unknown:` — it gates the start, and `tickets.py next` lists the ticket under `blocked` while the line is there; settle it with the user before the build begins, as a dated decision. Never resolve any of them by guessing.
|
||||
|
||||
## Reviewing or refining an existing ticket
|
||||
|
||||
Resolve what the user names with `uv run {skill-root}/scripts/tickets.py --project-root {project-root} find [<folder>] <ref>`: `<epic id>.<entry id>`, an entry id inside the epic folder, a tracker id, a file name, or an unbroken phrase from the title. When its `story_file` is null, pull the file with `tickets.py pull <epic folder> <id>` (same `--project-root`); `pull` writes a fixed layout, not the template. A done or dropped ticket and its plan are the record: do not reshape them to a newer template.
|
||||
|
||||
Full acceptance criteria are written here only for a bug, a ticket with no epic, and an entry that carries `refine = true`, which is set when the user asks for full criteria on that entry and is never proposed for a story. `tickets.py next` lists these under `ready_to_refine`. An epic's other stories go to the build as they are: the builder questions the user and writes the criteria into its plan. They are reviewed only when the user asks, and a request to refine one of them is a review unless the user asks for full criteria.
|
||||
|
||||
Where a change is written:
|
||||
|
||||
| Change | Written to |
|
||||
|---|---|
|
||||
| description, `Verify:`, criteria, references, notes | the file |
|
||||
| `title`, `type`, `after`, `hitl` | the file and the entry both; the file's `after` and `hitl` win, and `tickets.py status` reports a difference in either as `drift` |
|
||||
| full criteria asked for on a story | `refine = true` on the entry, `refined: false` in the file |
|
||||
| order | the tables in `tickets.toml` |
|
||||
|
||||
Once the ticket is published, every change also goes through `write`.
|
||||
|
||||
**Review** — the next story by default, several or all on request. Read the file, its source, and the finished siblings and their plans. With the user, improve the description, `Verify:`, references, and notes; adjust order and prerequisites, and run the set check in `validate.md` on the revised draft when they change. A review sets neither `status` nor `refined`: the file is intent that is ready for the build, which plans the story's criteria from the epic's Requirements and Done when and the story's description and `Verify:` check, which it may extend and never weaken.
|
||||
|
||||
**Refine** — before the ticket starts. Read the ticket, its source, the finished siblings and the plans beside them, and the code it will touch at the revision the work starts from. Where the source contradicts the code or another source, add `Source conflict: <id or section> — <what the source says> vs <what was found>` to the Notes of the container whose source it is and tell the user, offering to pass the correction to `bmad-spec` when the source is a BMad spec; a ticket covering that id stays `refined: false` until a `Decision:` line settles it. Confirm its prerequisites are done or in review, resolve questions that prevent implementation, and expand it per its type's template and "Rules the template cannot carry": criteria, boundaries, references, and decisions. Set `refined: true` when it passes self-review in full and the user approves it.
|
||||
|
||||
**A ticket that already carries full criteria, or a container** — read it and open what it references; query its remote state if it is published to a tracker. With the user: confirm they still agree with it; check its description against the current requirement source, criteria and references; find what is missing, unclear, or wrong; settle questions that prevent implementation. A container may end in a re-slice per `slice.md`.
|
||||
|
||||
A reply approves what was presented; publication and starting work are approved separately unless the user asks for them together. None of these changes status or assignee: preserve identity and any existing `status`, `tracker_status`, and assignee.
|
||||
|
||||
## Self-review before the user sees it
|
||||
|
||||
Read it back at its current level of detail: an entry needs its description and verification approach; a refined ticket needs runnable acceptance criteria and settled prerequisites. Check size (one agent session plans and finishes it), wording, references, and source consistency. Fix what you find; mention changes to the user's intent.
|
||||
|
||||
## After approval
|
||||
|
||||
On the repo store, save a new or changed ticket through the `write` verb: the commit it describes. On a tracker the ticket stays in the tree until it is published, a separate approval, per `board.md`. A ticket written or refined with full criteria carries `refined: true` once the user approves it, and gets the single-ticket check in `validate.md` before any build starts it.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Validation checks
|
||||
|
||||
One section per scope. `validate.md` says which sections an agent runs and what it is given.
|
||||
|
||||
## Ticket
|
||||
|
||||
- Every file or link referenced exists and opens; no unfilled placeholder; every assumption is marked.
|
||||
- Nothing contradicts the requirement source, its companions, the architecture, or a recorded decision.
|
||||
- An entry has a description, known uncertainty, and a `verify` check someone other than the builder can run. Full criteria are required before execution only for a bug, an entry with `refine = true`, and a ticket with no epic.
|
||||
- A ticket that needs full criteria and whose `covers` includes an id named in a `Source conflict:` line with no later `Decision:` settling it has `refined: false`.
|
||||
|
||||
## Set
|
||||
|
||||
The set is every entry in the epic's `tickets.toml` or draft breakdown, pulled or not.
|
||||
|
||||
- Together the tickets account for every requirement in the epic's spec, referenced numbered source, or Requirements, except scope deferred with the user. Each `covers` id exists there; several tickets may cover one id, each description saying which part it delivers.
|
||||
- UX, architecture, constraints, and integration work are accounted for even when they lack ids. The combined results must satisfy the epic's Done when; citing an id alone is not coverage.
|
||||
- Every `after` is a real prerequisite, those in other epics included, and none restates the order. Once `tickets.toml` exists, `tickets.py status` on the epic runs clean: no cycle, no `drift`, no `unpinned_after`, `undeclared_after`, or `order_conflict`. The tracer bullet (the first entry: the thinnest path through every layer the epic touches) and the sequencing decisions are in the epic's Notes.
|
||||
- Given the epic, an entry, and its references, a builder could write that story's acceptance criteria if it had to. If it could not, fix the epic's requirements or the references; the entry stays one sentence each for `description` and `verify`.
|
||||
- An epic of more than three entries closes with a "Refactor sweep" story that waits on every other entry except a closing end-to-end suite, or a `Decision:` line says why not.
|
||||
|
||||
## Tree
|
||||
|
||||
The tree is the initiative and its epic envelopes, drafted or written.
|
||||
|
||||
- Every initiative requirement has an accountable epic or an agreed deferral. Shared requirements say which part each epic delivers; no unexplained overlap.
|
||||
- Each epic's `covers` cites parent ids. Every id it assigns in Requirements or a local spec maps to one of them, and a child citing a local id resolves through that map to a parent id; an epic with empty `covers` cites a source section on every line instead. Verify the map resolves; a non-empty `covers` is not coverage.
|
||||
- Every unit the source touches is an epic, a touch point (a unit the work only consumes or configures, named in the initiative's Boundaries with the epic that owns the work there), or named out of scope.
|
||||
- Every decision two or more epics must adopt has one home: a spine section, or an entry in the opening epic (the first `[[epic]]`) that the others wait on.
|
||||
- Every epic envelope has Outcome, Done when, boundaries, upstream coverage, and references; a future epic needs neither a breakdown nor a completed spec. The initiative's `tickets.toml` lists every epic in build order, and each `after` names what is needed and from which epic. It opens with a platform-baseline epic (scaffold, environments, CI, deployment, operations), or a `Decision:` line says why not. Each epic's Done when includes production delivery.
|
||||
- With epics, no leaf sits directly under the initiative.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Missing prerequisites, run with the set and with the tree. An item is an entry in the set and an epic in the tree; a prerequisite is an `after` on either.
|
||||
|
||||
- Needs: list what must exist before each item can start and before its check can run: code, schema, setup, test tooling, fixtures, an entry point, a decision, an investigation's answer. The item builds it itself, or something in its `after` delivers it.
|
||||
- Collisions: two items with no dependency path between them can run at the same time, so they share no code, config, schema, or setup. Where they would, one goes in the other's `after` or they merge.
|
||||
- Shared setup: whatever more than one item needs (test harness, scaffold, schema, a shared component or contract, an integration) has one owner, the earliest item that needs it, and the others list it in `after`.
|
||||
- Handoffs: where one item relies on another's output (a default, an interface, a link target), both descriptions say so, so neither builder invents its own.
|
||||
|
||||
## Closure
|
||||
|
||||
- Verify the container's Done when against implementation evidence and its requirements, including constraints and companions. All known children done is evidence, not proof the parent is complete; explain any dropped work and confirm remaining scope with the user.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Validating tickets
|
||||
|
||||
The checks are in `{skill-root}/references/validate-checks.md`, one section per scope: Ticket, Set, Tree, Dependencies, Closure. Agents that were not in this conversation run them; each is given that file's path and the names of its sections, and reads them there. `tickets.py` reads only the prerequisites that were written; one that is missing is for these agents to find.
|
||||
|
||||
- **A proposed set of epics, a proposed epic breakdown, or a re-slice:** on the draft, before the user is asked to approve it. This always runs; no path, mode, or setting skips it. First walk the draft through the Dependencies section yourself and fix what that finds, then give the agent the corrected draft as text. Present the draft with what the check changed and what it confirmed. Run it again when the user's changes add, remove, merge, or reorder items. At initiative slicing, check the tree's scope ownership without requiring stories or full detail in future epics.
|
||||
- **A refined ticket:** before execution.
|
||||
- **A container:** before closure, its implemented coverage and Done when.
|
||||
|
||||
How:
|
||||
|
||||
- One subagent per epic: its container, the draft breakdown or `tickets.toml`, every child ticket including completed work, requirement source and companions, and the sections Ticket, Set, and Dependencies. One subagent for the tree: the initiative, the draft or written epic envelopes, source, and the sections Tree and Dependencies. A single ticket: one subagent with its source and the section Ticket. Closure: the section Closure.
|
||||
- A team's own checks are in `{workflow.checks}`, one array per scope with the same names; give each agent the entries for its sections as text.
|
||||
- Where tickets carry full criteria (`refined: true`), say which they are and point the agent at "Rules the template cannot carry" in `{skill-root}/references/ticket.md` for the criteria and risk rules. Once `tickets.toml` exists, give the full `tickets.py status` command for the folder.
|
||||
- Merge findings into fix (mechanical), suggest (a guideline, with its reason), or ask (needs the user). Resolve coverage gaps, missing prerequisites, and contradictions before proceeding; the user decides suggestions and scope changes.
|
||||
- A declined suggestion recorded as a `Decision:` line is not raised again unless new evidence changes its basis.
|
||||
|
||||
The user may request any scope independently.
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# ///
|
||||
"""Read the ticket store's config without loading the rest into context.
|
||||
|
||||
The config is two layers. The base is the skill's starter for the store, `config/<store>-ticketing.toml`.
|
||||
Over it goes the project's `_bmad/custom/ticketing-store-config.toml`: its `[tickets] store` names the
|
||||
store, and any other key it sets replaces the starter's, tables merging key by key. With no project file
|
||||
the store is `repo`. A store with no starter is read from the project file alone.
|
||||
|
||||
read_store.py --project-root <root> [-k <dotted key> ...]
|
||||
read_store.py --starters
|
||||
|
||||
One key prints its value, a string bare and anything else as JSON; several print one JSON object; none
|
||||
prints the whole merged config. `--starters` prints each shipped store with its description.
|
||||
Exit 1 when a file cannot be read, 2 when a key is missing.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
_MISSING = object()
|
||||
STARTERS = Path(__file__).resolve().parents[1] / "config"
|
||||
PROJECT_FILE = Path("_bmad") / "custom" / "ticketing-store-config.toml"
|
||||
STORE_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
|
||||
|
||||
def load(path: Path) -> dict:
|
||||
# utf-8-sig: Windows editors can save a byte-order mark.
|
||||
return tomllib.loads(path.read_text(encoding="utf-8-sig"))
|
||||
|
||||
|
||||
def merge(base: dict, over: dict) -> dict:
|
||||
out = dict(base)
|
||||
for key, value in over.items():
|
||||
out[key] = merge(out[key], value) if isinstance(value, dict) and isinstance(out.get(key), dict) else value
|
||||
return out
|
||||
|
||||
|
||||
def store_config(project_root: Path, starters: Path) -> dict:
|
||||
path = project_root / PROJECT_FILE
|
||||
project = load(path) if path.is_file() else {}
|
||||
tickets = project.get("tickets")
|
||||
store = tickets.get("store") if isinstance(tickets, dict) else None
|
||||
if not isinstance(store, str) or not store:
|
||||
store = "repo" # as tickets.py reads it
|
||||
starter = starters / f"{store}-ticketing.toml"
|
||||
config = merge(load(starter), project) if STORE_RE.match(store) and starter.is_file() else project
|
||||
if isinstance(config.get("tickets"), dict):
|
||||
config["tickets"]["store"] = store
|
||||
return config
|
||||
|
||||
|
||||
def extract(data, dotted: str):
|
||||
current = data
|
||||
for part in dotted.split("."):
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else:
|
||||
return _MISSING
|
||||
return current
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--project-root", help="project holding _bmad/")
|
||||
parser.add_argument("--key", "-k", action="append", default=[], help="dotted key (repeatable)")
|
||||
parser.add_argument("--starters", action="store_true", help="list the shipped stores and their descriptions")
|
||||
parser.add_argument(
|
||||
"--starters-dir", default=str(STARTERS), help="folder of starters; default: the skill's config/"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
starters = Path(args.starters_dir).expanduser()
|
||||
|
||||
try:
|
||||
if args.starters:
|
||||
found = {}
|
||||
for path in sorted(starters.glob("*-ticketing.toml")):
|
||||
data = load(path)
|
||||
found[data.get("tickets", {}).get("store", path.name)] = data.get("description", "")
|
||||
print(json.dumps(found, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
if not args.project_root:
|
||||
parser.error("--project-root is required")
|
||||
data = store_config(Path(args.project_root).expanduser(), starters)
|
||||
except (OSError, ValueError) as error: # ValueError covers a TOML or encoding failure
|
||||
sys.stderr.write(f"error: cannot read the store config: {error}\n")
|
||||
return 1
|
||||
|
||||
if not args.key:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
found = {}
|
||||
missing = []
|
||||
for key in args.key:
|
||||
value = extract(data, key)
|
||||
(missing.append(key) if value is _MISSING else found.__setitem__(key, value))
|
||||
for key in missing:
|
||||
sys.stderr.write(f"missing: {key}\n")
|
||||
|
||||
if len(args.key) == 1:
|
||||
if missing:
|
||||
return 2
|
||||
value = found[args.key[0]]
|
||||
print(value.rstrip("\n") if isinstance(value, str) else json.dumps(value, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(json.dumps(found, indent=2, ensure_ascii=False))
|
||||
return 2 if missing else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
# Piped output on Windows defaults to a legacy code page, not UTF-8.
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "read_store.py"
|
||||
|
||||
REPO = '''
|
||||
# comment that must never be printed
|
||||
description = "Files in the repo."
|
||||
|
||||
[tickets]
|
||||
store = "repo"
|
||||
access = "Read and write the files."
|
||||
|
||||
[verbs]
|
||||
write = """
|
||||
Edit the file.
|
||||
Commit it.
|
||||
"""
|
||||
|
||||
[tickets.status]
|
||||
backlog = "backlog"
|
||||
done = "done"
|
||||
'''
|
||||
|
||||
JIRA = """
|
||||
description = "Jira through acli."
|
||||
|
||||
[tickets]
|
||||
store = "jira"
|
||||
key = ""
|
||||
site = ""
|
||||
|
||||
[verbs]
|
||||
write = "acli create"
|
||||
query = "acli view"
|
||||
|
||||
[tickets.status]
|
||||
backlog = "To Do"
|
||||
review = "In Review"
|
||||
done = "Done"
|
||||
"""
|
||||
|
||||
|
||||
class ReadStoreTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.tmp.name) / "project"
|
||||
self.starters = Path(self.tmp.name) / "config"
|
||||
(self.root / "_bmad" / "custom").mkdir(parents=True)
|
||||
self.starters.mkdir()
|
||||
(self.starters / "repo-ticketing.toml").write_text(REPO, encoding="utf-8")
|
||||
(self.starters / "jira-ticketing.toml").write_text(JIRA, encoding="utf-8")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def project(self, text):
|
||||
(self.root / "_bmad" / "custom" / "ticketing-store-config.toml").write_text(text, encoding="utf-8")
|
||||
|
||||
def run_script(self, *args):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--starters-dir", str(self.starters), *args],
|
||||
encoding="utf-8",
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def read(self, *keys):
|
||||
args = [a for k in keys for a in ("-k", k)]
|
||||
return self.run_script("--project-root", str(self.root), *args)
|
||||
|
||||
def test_no_project_file_reads_the_repo_starter(self):
|
||||
result = self.read("tickets.store")
|
||||
self.assertEqual((result.returncode, result.stdout), (0, "repo\n"))
|
||||
result = self.read("verbs.write")
|
||||
self.assertEqual(result.stdout, "Edit the file.\nCommit it.\n")
|
||||
self.assertNotIn("comment", self.read().stdout)
|
||||
|
||||
def test_the_project_file_names_the_store_and_its_keys_win_one_by_one(self):
|
||||
self.project('[tickets]\nstore = "jira"\nkey = "SHOP"\n\n[tickets.status]\nreview = ""\n')
|
||||
result = self.read("tickets", "verbs.query")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
json.loads(result.stdout),
|
||||
{
|
||||
"tickets": {
|
||||
"store": "jira",
|
||||
"key": "SHOP",
|
||||
"site": "",
|
||||
"status": {"backlog": "To Do", "review": "", "done": "Done"},
|
||||
},
|
||||
"verbs.query": "acli view",
|
||||
},
|
||||
)
|
||||
|
||||
def test_a_verb_the_project_file_sets_replaces_the_starters(self):
|
||||
self.project('[tickets]\nstore = "jira"\n\n[verbs]\nwrite = "our own create"\n')
|
||||
self.assertEqual(self.read("verbs.write").stdout, "our own create\n")
|
||||
self.assertEqual(self.read("verbs.query").stdout, "acli view\n")
|
||||
|
||||
def test_a_store_with_no_starter_is_read_from_the_project_file_alone(self):
|
||||
self.project('[tickets]\nstore = "azure"\n\n[verbs]\nwrite = "az boards"\n')
|
||||
self.assertEqual(self.read("verbs.write").stdout, "az boards\n")
|
||||
result = self.read("verbs.query")
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("missing: verbs.query", result.stderr)
|
||||
|
||||
def test_a_store_name_that_is_no_file_name_reads_no_starter(self):
|
||||
self.project('[tickets]\nstore = "../repo"\n')
|
||||
self.assertEqual(self.read("verbs.write").returncode, 2)
|
||||
|
||||
def test_a_project_file_that_names_no_store_reads_the_repo_starter(self):
|
||||
for text in ("[workflow]\nx = 1\n", "[tickets]\nstore = 5\n", '[tickets]\nstore = ""\nkey = "K"\n'):
|
||||
self.project(text)
|
||||
self.assertEqual(self.read("verbs.write").stdout, "Edit the file.\nCommit it.\n", text)
|
||||
self.assertEqual(self.read("tickets.store").stdout, "repo\n", text)
|
||||
self.assertEqual(self.read("tickets.key").stdout, "K\n")
|
||||
|
||||
def test_several_keys_with_one_missing_print_the_rest_and_exit_2(self):
|
||||
result = self.read("tickets.store", "tickets.nope")
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertEqual(json.loads(result.stdout), {"tickets.store": "repo"})
|
||||
self.assertIn("missing: tickets.nope", result.stderr)
|
||||
|
||||
def test_an_unreadable_project_file_exits_1(self):
|
||||
self.project("[tickets\nstore = ")
|
||||
result = self.read("tickets.store")
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("cannot read the store config", result.stderr)
|
||||
|
||||
def test_starters_lists_each_store_with_its_description(self):
|
||||
result = self.run_script("--starters")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(json.loads(result.stdout), {"jira": "Jira through acli.", "repo": "Files in the repo."})
|
||||
|
||||
def test_every_shipped_starter_parses_and_is_named_for_its_store(self):
|
||||
shipped = SCRIPT.parents[1] / "config"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--starters"], encoding="utf-8", capture_output=True, check=False
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
sorted(f"{store}-ticketing.toml" for store in json.loads(result.stdout)),
|
||||
sorted(p.name for p in shipped.glob("*-ticketing.toml")),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user