anthropic critique of skill adjustments
This commit is contained in:
parent
dd1efb3c61
commit
1136a59e0c
|
|
@ -11,16 +11,14 @@ This skill produces and maintains the **v7 epic-first folder tree** for an initi
|
||||||
|
|
||||||
**Acts as:** a product strategist and technical specifications writer collaborating with the user as a peer. The user owns product vision and priorities; this skill brings requirements decomposition, sizing judgment, and the v7 schema. Conversational throughout — soft gates ("ready to move on?") rather than rigid menus.
|
**Acts as:** a product strategist and technical specifications writer collaborating with the user as a peer. The user owns product vision and priorities; this skill brings requirements decomposition, sizing judgment, and the v7 schema. Conversational throughout — soft gates ("ready to move on?") rather than rigid menus.
|
||||||
|
|
||||||
**One skill, three modes × three interaction styles:**
|
**One skill, three modes:**
|
||||||
|
|
||||||
- **Create** — no `epics/` tree yet. Walks intent → discovery → epic design → per-epic authoring → validate → finalize.
|
- **Create** — no `epics/` tree yet. Walks intent → discovery → epic design → per-epic authoring → validate → finalize.
|
||||||
- **Edit** — the tree exists. Routes by user phrasing or flag to add-epic, split-epic, merge-epics, rename-epic, refine-story, re-derive-deps, or re-validate. Never re-walks intent or discovery.
|
- **Edit** — the tree exists. Routes by user phrasing or flag to add-epic, split-epic, merge-epics, rename-epic, refine-story, re-derive-deps, or re-validate. Never re-walks intent or discovery.
|
||||||
- **Migrate** — a v6 monolithic `epics.md` (or sharded directory) exists but no v7 tree. Offers leave-alone, run-canonical-helper, or walk-through-manually.
|
- **Migrate** — a v6 monolithic `epics.md` (or sharded directory) exists but no v7 tree. Offers leave-alone, run-canonical-helper, or walk-through-manually.
|
||||||
|
|
||||||
Interaction style is orthogonal:
|
Plus a **deterministic surface** for pipelines:
|
||||||
|
|
||||||
- **Guided** (default) — conversational dialog with soft gates and per-epic checkpoints. Right for first-timers and complex initiatives.
|
|
||||||
- **YOLO** (`--yolo`) — same flow, but discovery summary, the soft gate dialog at each stage, and the per-epic checkpoint are skipped. The skill proposes the full epic list in one shot, authors every epic end-to-end, then surfaces a single batched recap before validation. Right for experts on their third initiative this quarter.
|
|
||||||
- **From-spec** (`--from-spec <path>`) — Stages 1–3 skipped entirely. A structured spec drives Stages 4 and 5 deterministically. Right for pipelines and pre-drafted plans.
|
- **From-spec** (`--from-spec <path>`) — Stages 1–3 skipped entirely. A structured spec drives Stages 4 and 5 deterministically. Right for pipelines and pre-drafted plans.
|
||||||
|
|
||||||
**Headless surfaces:**
|
**Headless surfaces:**
|
||||||
|
|
@ -28,7 +26,7 @@ Interaction style is orthogonal:
|
||||||
- `--re-validate` (alias `--headless` / `-H`) runs strict validation only and emits JSON. Pair with `--coverage-strict` to fail CI on uncovered requirements.
|
- `--re-validate` (alias `--headless` / `-H`) runs strict validation only and emits JSON. Pair with `--coverage-strict` to fail CI on uncovered requirements.
|
||||||
- `--from-spec <path>` runs end-to-end authoring + validation deterministically and emits JSON. Implicitly headless; pass `--coverage-strict` to fail on uncovered requirements.
|
- `--from-spec <path>` runs end-to-end authoring + validation deterministically and emits JSON. Implicitly headless; pass `--coverage-strict` to fail on uncovered requirements.
|
||||||
|
|
||||||
All other modes are interactive (Guided and YOLO).
|
Create, edit, and migrate are interactive.
|
||||||
|
|
||||||
**Owns:** front-matter schemas (`resources/`), bootstrap and validation scripts (`scripts/`), the inventory cache at `{initiative_store}/.bmad-cache/inventory.json`, and the only writers of the epic tree. **Does not own:** `governance.md` or `initiative-context.md` authoring, `initiative_store` config plumbing, downstream status transitions beyond `draft`.
|
**Owns:** front-matter schemas (`resources/`), bootstrap and validation scripts (`scripts/`), the inventory cache at `{initiative_store}/.bmad-cache/inventory.json`, and the only writers of the epic tree. **Does not own:** `governance.md` or `initiative-context.md` authoring, `initiative_store` config plumbing, downstream status transitions beyond `draft`.
|
||||||
|
|
||||||
|
|
@ -90,11 +88,7 @@ If the user passed `--re-validate`, `--headless`, or `-H` (or said "re-validate"
|
||||||
|
|
||||||
If the user passed `--from-spec <path>`, set `{mode}=from-spec` and `{spec_path}=<path>`. Set `{headless_mode}=true` by default (override only if the user is interactive). Skip Stages 1–3, route directly to `prompts/from-spec.md`.
|
If the user passed `--from-spec <path>`, set `{mode}=from-spec` and `{spec_path}=<path>`. Set `{headless_mode}=true` by default (override only if the user is interactive). Skip Stages 1–3, route directly to `prompts/from-spec.md`.
|
||||||
|
|
||||||
### 3. YOLO interaction style
|
### 3. Mode by filesystem state
|
||||||
|
|
||||||
If the user passed `--yolo` (or said "yolo this", "go fast", "don't ask me", or similar in their opening message), set `{yolo}=true`. Otherwise `{yolo}=false`. YOLO is orthogonal to the mode — both create and migrate flows respect it. Edit-mode sub-flows ignore `{yolo}` because they are inherently interactive graph reasoning.
|
|
||||||
|
|
||||||
### 4. Mode by filesystem state
|
|
||||||
|
|
||||||
- If `{initiative_store}/epics/` does not exist OR exists but contains no epic folders → `{mode}=create`.
|
- If `{initiative_store}/epics/` does not exist OR exists but contains no epic folders → `{mode}=create`.
|
||||||
- If `{initiative_store}/epics/` contains v7 epic folders (any folder matching `NN-*` with an `epic.md` inside) → `{mode}=edit`.
|
- If `{initiative_store}/epics/` contains v7 epic folders (any folder matching `NN-*` with an `epic.md` inside) → `{mode}=edit`.
|
||||||
|
|
@ -102,7 +96,7 @@ If the user passed `--yolo` (or said "yolo this", "go fast", "don't ask me", or
|
||||||
|
|
||||||
If both v7 folders and a v6 file exist, prefer `edit` and surface the v6 file in Stage 1 as a one-line note.
|
If both v7 folders and a v6 file exist, prefer `edit` and surface the v6 file in Stage 1 as a one-line note.
|
||||||
|
|
||||||
### 5. Edit sub-mode dispatch (only when `{mode}=edit`)
|
### 4. Edit sub-mode dispatch (only when `{mode}=edit`)
|
||||||
|
|
||||||
Detect from the user's opening message:
|
Detect from the user's opening message:
|
||||||
|
|
||||||
|
|
@ -120,7 +114,7 @@ Detect from the user's opening message:
|
||||||
|
|
||||||
Set `{edit_submode}` to the matched value before routing.
|
Set `{edit_submode}` to the matched value before routing.
|
||||||
|
|
||||||
### 6. Route
|
### 5. Route
|
||||||
|
|
||||||
- `create` → `prompts/intent.md`
|
- `create` → `prompts/intent.md`
|
||||||
- `migrate` → `prompts/intent.md` (it offers the migrate three-options branch when `{mode}=migrate`)
|
- `migrate` → `prompts/intent.md` (it offers the migrate three-options branch when `{mode}=migrate`)
|
||||||
|
|
@ -128,7 +122,7 @@ Set `{edit_submode}` to the matched value before routing.
|
||||||
- `headless` → `prompts/validate.md`
|
- `headless` → `prompts/validate.md`
|
||||||
- `from-spec` → `prompts/from-spec.md`
|
- `from-spec` → `prompts/from-spec.md`
|
||||||
|
|
||||||
Carry `{mode}`, `{yolo}`, `{spec_path}` (when set), and `{edit_submode}` (when set) into the routed prompt.
|
Carry `{mode}`, `{spec_path}` (when set), and `{edit_submode}` (when set) into the routed prompt.
|
||||||
|
|
||||||
## Stages
|
## Stages
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
## Pre-flight
|
## Pre-flight
|
||||||
|
|
||||||
Before launching the artifact-analyzer, tell the user (in 3–5 lines) what you're about to scan: the resolved `{planning_artifacts}` path, the resolved `{project_knowledge}` path, and any user-pointed paths from Stage 1. This lets a misconfigured path surface immediately rather than as an empty result. Skip the pre-flight in `{yolo}=true` and `{mode}=headless`.
|
Before launching the artifact-analyzer, tell the user (in 3–5 lines) what you're about to scan: the resolved `{planning_artifacts}` path, the resolved `{project_knowledge}` path, and any user-pointed paths from Stage 1. This lets a misconfigured path surface immediately rather than as an empty result. Skip the pre-flight in `{mode}=headless`.
|
||||||
|
|
||||||
## Subagent fan-out
|
## Subagent fan-out
|
||||||
|
|
||||||
|
|
@ -59,12 +59,10 @@ Lists may be empty. Each requirement entry must have a unique `code`.
|
||||||
|
|
||||||
Tell the user in 4–8 lines: counts (FRs, NFRs, UX-DRs, debt items), the starter-template note if any, governance constraints if any, and any gaps. Do not dump the full inventory — they have the source documents. Mention the cache path so they know where the inventory lives.
|
Tell the user in 4–8 lines: counts (FRs, NFRs, UX-DRs, debt items), the starter-template note if any, governance constraints if any, and any gaps. Do not dump the full inventory — they have the source documents. Mention the cache path so they know where the inventory lives.
|
||||||
|
|
||||||
In `{yolo}=true` collapse to a single line: "Inventory: N FRs, M NFRs, K UX-DRs (cached at `.bmad-cache/inventory.json`)."
|
|
||||||
|
|
||||||
In `{mode}=headless` skip the summary entirely.
|
In `{mode}=headless` skip the summary entirely.
|
||||||
|
|
||||||
Soft gate (interactive only): "Anything missing or wrong here, or shall we move on to designing the epic list?"
|
Soft gate (interactive only): "Anything missing or wrong here, or shall we move on to designing the epic list?"
|
||||||
|
|
||||||
## Stage Complete
|
## Stage Complete
|
||||||
|
|
||||||
When the user confirms (or `{yolo}=true` auto-confirms), route to `prompts/epic-design.md`. The inventory remains on disk; later stages re-read it rather than relying on working memory.
|
When the user confirms, route to `prompts/epic-design.md`. The inventory remains on disk; later stages re-read it rather than relying on working memory.
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,6 @@ python3 scripts/validate_initiative.py --initiative-store {initiative_store} --l
|
||||||
|
|
||||||
Before starting the next epic, confirm with the user that this epic is complete. The next epic does not begin until the current is approved.
|
Before starting the next epic, confirm with the user that this epic is complete. The next epic does not begin until the current is approved.
|
||||||
|
|
||||||
In `{yolo}=true`, **skip the per-epic checkpoint** entirely — author the full epic list end-to-end, then surface a single batched recap before routing to validation.
|
|
||||||
|
|
||||||
## After all epics are authored
|
## After all epics are authored
|
||||||
|
|
||||||
Route to `prompts/validate.md` for full-tree strict validation.
|
Route to `prompts/validate.md` for full-tree strict validation.
|
||||||
|
|
@ -106,4 +104,4 @@ After any edit-mode flow finishes, route to `prompts/validate.md` strict.
|
||||||
|
|
||||||
## Stage Complete
|
## Stage Complete
|
||||||
|
|
||||||
Stage 4 ends when every approved epic has its `epic.md` and all its story files written, the per-epic non-strict validation passes for each, and the user has confirmed completion (or `{yolo}=true` auto-confirmed after the batch recap).
|
Stage 4 ends when every approved epic has its `epic.md` and all its story files written, the per-epic non-strict validation passes for each, and the user has confirmed completion.
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,9 @@ Mentally compute the cross-epic dependency graph. If you find any cycle (Epic A
|
||||||
|
|
||||||
If the user wants to pressure-test the epic shape, they may invoke `bmad-advanced-elicitation` (deeper critique methods) or `bmad-party-mode` (multi-agent perspectives) explicitly. **Do not present these as a menu** — only invoke when the user asks.
|
If the user wants to pressure-test the epic shape, they may invoke `bmad-advanced-elicitation` (deeper critique methods) or `bmad-party-mode` (multi-agent perspectives) explicitly. **Do not present these as a menu** — only invoke when the user asks.
|
||||||
|
|
||||||
## YOLO mode
|
|
||||||
|
|
||||||
When `{yolo}=true`, propose the entire epic list in one message — title, intent, `depends_on`, theme, and FR/UX-DR allocations for every epic — and ask the user once whether to lock it in or revise. Skip the per-step dialog; rely on the cycle check and Stage 5 to catch problems.
|
|
||||||
|
|
||||||
## Soft gate
|
## Soft gate
|
||||||
|
|
||||||
"Does this epic list capture the initiative? Anything missing, anything overlapping that should be consolidated?" When the user is satisfied, the list is approved and Stage 3 is complete. Skip in `{yolo}=true` after the one-shot proposal is approved.
|
"Does this epic list capture the initiative? Anything missing, anything overlapping that should be consolidated?" When the user is satisfied, the list is approved and Stage 3 is complete.
|
||||||
|
|
||||||
## Edit-mode flows
|
## Edit-mode flows
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,6 @@ After the user picks:
|
||||||
|
|
||||||
This stage is conversational. Confirm with a soft prompt rather than a menu — "Anything else to add about the initiative, or should we move on to scanning the project?" Users almost always remember one more thing when given a graceful exit ramp.
|
This stage is conversational. Confirm with a soft prompt rather than a menu — "Anything else to add about the initiative, or should we move on to scanning the project?" Users almost always remember one more thing when given a graceful exit ramp.
|
||||||
|
|
||||||
In `{yolo}=true` skip the soft gate entirely once the three items are settled.
|
|
||||||
|
|
||||||
## Stage Complete
|
## Stage Complete
|
||||||
|
|
||||||
Stage 1 ends when the chosen mode's exit conditions above are met. Carry the initiative title, primary intent, story-type mix, and any volunteered details into the next stage in working memory — none of this is written to disk yet (Stage 2 writes the inventory to `.bmad-cache/inventory.json`).
|
Stage 1 ends when the chosen mode's exit conditions above are met. Carry the initiative title, primary intent, story-type mix, and any volunteered details into the next stage in working memory — none of this is written to disk yet (Stage 2 writes the inventory to `.bmad-cache/inventory.json`).
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ If `{mode}=headless`:
|
||||||
|
|
||||||
### 1. Surface failures conversationally
|
### 1. Surface failures conversationally
|
||||||
|
|
||||||
For each error in `findings`, explain it in one sentence and offer to fix. Group by file when several errors land on the same path. Common patterns and the right next step:
|
For each error in `findings`, explain it in one sentence and offer to fix. Group by file when several errors land on the same path. The full code → meaning → fix lookup lives at `resources/validation-error-codes.md` — load it when surfacing failures so the explanation and fix are accurate. Common shortcuts to the right next step:
|
||||||
|
|
||||||
- **Schema errors** (`*-extra-keys`, `*-missing-keys`, `*-bad-status`, `*-bad-type`) → loop back to `prompts/epic-authoring.md` for that one file, edit the front matter, re-validate.
|
- **Schema errors** (`*-extra-keys`, `*-missing-keys`, `*-bad-status`, `*-bad-type`) → loop back to `prompts/epic-authoring.md` for that one file, edit the front matter, re-validate.
|
||||||
- **`epic-nn-mismatch` / `story-epic-mismatch`** → likely a hand-edit of the front matter; the folder name is canonical, so update the front matter to match.
|
- **`epic-nn-mismatch` / `story-epic-mismatch`** → likely a hand-edit of the front matter; the folder name is canonical, so update the front matter to match.
|
||||||
|
|
@ -45,8 +45,6 @@ For each error in `findings`, explain it in one sentence and offer to fix. Group
|
||||||
- **`epic-dep-cycle`** → cross-epic graph has a loop. Loop back to `prompts/epic-design.md` (re-derive-deps flow) to fix it.
|
- **`epic-dep-cycle`** → cross-epic graph has a loop. Loop back to `prompts/epic-design.md` (re-derive-deps flow) to fix it.
|
||||||
- **`story-numbering-gaps`** → use `scripts/rename_story.py --to-nn` to fill the gap or renumber the survivors.
|
- **`story-numbering-gaps`** → use `scripts/rename_story.py --to-nn` to fill the gap or renumber the survivors.
|
||||||
|
|
||||||
If the failure-pattern set ever grows past ~10 entries, extract this list to `resources/validation-error-codes.md` to keep this prompt tight.
|
|
||||||
|
|
||||||
### 2. Coverage check
|
### 2. Coverage check
|
||||||
|
|
||||||
When `--inventory` was passed, the validator already produced `coverage_missing` deterministically — surface those codes conversationally and route into the **coverage-fix** edit-mode entry point in `prompts/epic-authoring.md`.
|
When `--inventory` was passed, the validator already produced `coverage_missing` deterministically — surface those codes conversationally and route into the **coverage-fix** edit-mode entry point in `prompts/epic-authoring.md`.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ metadata: # OPTIONAL — free-form table; BMad
|
||||||
- **title** — Always emitted with double quotes by `init_story.py`. Inner double quotes escaped with `\`.
|
- **title** — Always emitted with double quotes by `init_story.py`. Inner double quotes escaped with `\`.
|
||||||
- **type** — Drives body-skeleton generation: `task` omits the As-a/I-want/So-that stanza by default; `bug` and `spike` make it optional; `feature` requires it.
|
- **type** — Drives body-skeleton generation: `task` omits the As-a/I-want/So-that stanza by default; `bug` and `spike` make it optional; `feature` requires it.
|
||||||
- **status** — `init_story.py` always writes `draft`. Promotion to any other value is owned by downstream skills (`bmad-dev-story` etc.). This skill never auto-promotes.
|
- **status** — `init_story.py` always writes `draft`. Promotion to any other value is owned by downstream skills (`bmad-dev-story` etc.). This skill never auto-promotes.
|
||||||
- **epic** — The enclosing folder name (e.g. `01-billing-stripe`), not just the NN. The folder name wins on conflict; the validator flags drift.
|
- **epic** — The enclosing folder name (e.g. `01-billing-stripe`), not just the NN. Emitted unquoted (the dash makes it unambiguously a string in YAML). The folder name wins on conflict; the validator flags drift.
|
||||||
- **depends_on** — Inline YAML list. Two reference forms:
|
- **depends_on** — Inline YAML list. Two reference forms:
|
||||||
- **Within-epic:** the sibling story's basename without `.md` — e.g. `04-define-schema`.
|
- **Within-epic:** the sibling story's basename without `.md` — e.g. `04-define-schema`.
|
||||||
- **Cross-epic:** `<epic-folder>/<story-basename>` — e.g. `02-auth-migration/04-session-management`.
|
- **Cross-epic:** `<epic-folder>/<story-basename>` — e.g. `02-auth-migration/04-session-management`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
# Validation Error Codes
|
||||||
|
|
||||||
|
The lookup table for every error and warning `scripts/validate_initiative.py` emits — what each code means, how it arises, and how to fix it. Loaded by `prompts/validate.md` when surfacing failures conversationally.
|
||||||
|
|
||||||
|
The output shape is always:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"level": "error|warning", "code": "<code>", "message": "<human text>", "path": "<absolute path>"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`level` controls the exit code: any `error` causes the validator to return 1; warnings don't. `coverage-missing` is the one code whose level depends on a flag (`--coverage-strict` escalates it).
|
||||||
|
|
||||||
|
## Schema errors
|
||||||
|
|
||||||
|
These fire when a file's front matter doesn't match the locked schema in `resources/epic-frontmatter-schema.md` and `resources/story-frontmatter-schema.md`.
|
||||||
|
|
||||||
|
| Code | Meaning | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `epic-frontmatter-parse` | The validator's loose YAML parser couldn't read the front matter. Usually a missing `:`, an unindented continuation, or unclosed `---`. | Open the `epic.md` and check the front-matter block. The parser tolerates inline lists and inline strings; reject indented multi-line strings. |
|
||||||
|
| `epic-extra-keys` | The epic's front matter has a top-level key that isn't in `{title, epic, status, depends_on, metadata}`. | Move the key under `metadata:` (which is free-form), or remove it. |
|
||||||
|
| `epic-missing-keys` | A required top-level key is absent: `title`, `epic`, `status`, or `depends_on`. | Add it. `init_epic.py` always emits all four; this fires when a hand-edit dropped one. |
|
||||||
|
| `epic-bad-status` | `status` value isn't one of `draft / ready / in-progress / review / done / blocked`. | Set to a valid enum value. New epics should be `draft`. |
|
||||||
|
| `epic-nn-mismatch` | `epic:` field doesn't match the folder's NN prefix. The folder name is canonical. | Edit the front matter to match the folder. If the folder name itself is wrong, use `rename_epic.py --to-nn`. |
|
||||||
|
| `epic-deps-not-list` | `depends_on:` is not a YAML list. | Make it `[]` (empty) or `["01", "02"]`. Inline lists only. |
|
||||||
|
| `story-frontmatter-parse` | Same as above, for a story file. | Check the front-matter block. |
|
||||||
|
| `story-extra-keys` | Story has a top-level key not in `{title, type, status, epic, depends_on, metadata}`. | Move under `metadata:` or remove. |
|
||||||
|
| `story-missing-keys` | A required key is absent. | Add it. |
|
||||||
|
| `story-bad-type` | `type` isn't one of `feature / bug / task / spike`. | Pick one. Story type drives body skeleton choices (the user-story stanza is required for `feature`, optional for `bug`/`spike`, absent for `task`). |
|
||||||
|
| `story-bad-status` | Same as `epic-bad-status` for a story. | Set a valid status. |
|
||||||
|
| `story-bad-prefix` | Story filename doesn't start with `NN-`. | Rename to match the convention. The validator expects `^\d+-`. |
|
||||||
|
| `story-epic-mismatch` | Story's `epic:` field doesn't match its enclosing folder name. The folder name is canonical. | Edit the front matter to match the folder. Or move the file with `move_story.py`. |
|
||||||
|
| `story-deps-not-list` | `depends_on:` is not a YAML list. | Make it `[]` or an inline list. |
|
||||||
|
|
||||||
|
## Dependency errors
|
||||||
|
|
||||||
|
These fire when references don't resolve.
|
||||||
|
|
||||||
|
| Code | Meaning | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `epic-dep-unresolved` | An epic's `depends_on` references an NN that has no corresponding folder in the tree. | Either fix the typo, or remove the dep, or create the missing epic. |
|
||||||
|
| `epic-dep-cycle` | The cross-epic depends_on graph has a cycle (Epic A → B → A, directly or transitively). The message lists the cycle. | Break the cycle. If the cycle reflects a real bidirectional dependency, the epics should probably be merged or the seam reconsidered. |
|
||||||
|
| `story-dep-unresolved` | A story's `depends_on` entry doesn't resolve — either as a within-epic basename or as a cross-epic `<folder>/<basename>` ref. | Fix the typo, or use `move_story.py` / `rename_story.py` going forward (they update refs atomically). For after-the-fact fixes, edit `depends_on:` directly and re-validate. |
|
||||||
|
|
||||||
|
## Structural errors
|
||||||
|
|
||||||
|
| Code | Meaning | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `no-epics-dir` | `{initiative_store}/epics/` doesn't exist. | Either you have the wrong `--initiative-store`, or no tree has been generated yet. Run the skill in create mode. |
|
||||||
|
| `missing-epic-md` | A folder matches `NN-*` but has no `epic.md` inside. | Either the folder is bogus (delete it) or the `epic.md` was lost (regenerate with `init_epic.py` and re-fill). |
|
||||||
|
| `story-numbering-gaps` | Story NNs in an epic are not sequential `01..N`. The message lists what was found vs expected. | Use `rename_story.py --to-nn` to fill the gap or renumber the survivors. |
|
||||||
|
|
||||||
|
## Coverage findings
|
||||||
|
|
||||||
|
| Code | Meaning | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `coverage-missing` | An inventory code from `--inventory <file>` does not appear textually in any story body. **Warning** by default; **error** under `--coverage-strict`. | Use the `coverage-fix` edit-mode entry point in `prompts/edit-mode.md` — extend an existing story's `## Coverage` section, or add a new story for the missing code. |
|
||||||
|
|
||||||
|
## Sizing warnings
|
||||||
|
|
||||||
|
| Code | Meaning | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `story-oversized` (warning) | A story's body is more than 3× the epic mean (computed when the epic has ≥ 3 stories). | Often a real signal that the story should be split. Sometimes the threshold is too tight for legitimately-large foundational stories — judgment call. The warning never fails CI. Pass `--lax` to suppress mid-flow. |
|
||||||
|
|
||||||
|
## Validator runtime errors
|
||||||
|
|
||||||
|
These exit `2` and aren't structured findings.
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `template missing: ...` | `resources/epic-md-template.md` or `resources/story-md-template.md` not found. | Reinstall the skill; resources are required. |
|
||||||
|
| `inventory file not found: ...` | `--inventory <path>` was passed but the file doesn't exist. | Either the path is wrong, or the inventory cache was deleted. The cache lives at `{initiative_store}/.bmad-cache/inventory.json` and is auto-deleted at finalize — that's expected. |
|
||||||
|
| `could not parse <path>: ...` | The inventory file isn't valid JSON. | Fix the JSON. |
|
||||||
|
|
||||||
|
## from-spec errors
|
||||||
|
|
||||||
|
`scripts/from_spec.py` validates the spec before doing any work; on failure it returns:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"error": "invalid spec", "details": ["epic[0] missing `title`", "..."]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common shapes:
|
||||||
|
|
||||||
|
| Detail | Cause | Fix |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `spec must contain a non-empty epics list` | Top-level `epics` is missing or empty. | Add at least one epic. |
|
||||||
|
| `epic[N] missing nn` / `missing title` | A required epic field is absent. | Add it. |
|
||||||
|
| `epic[N].stories[M] missing nn / title / type` | A required story field is absent. | Add it. |
|
||||||
|
| `epic[N].stories[M].type invalid: '...'` | `type` is not `feature` / `task` / `bug` / `spike`. | Pick a valid type. |
|
||||||
|
|
||||||
|
If the spec is valid but `init_epic.py` or `init_story.py` fails (e.g. a folder collision), the envelope returns:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"error": "init_epic.py failed", "details": "epic folder already exists: ...", "epic": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usually means the target initiative store isn't empty. Either clean it (`rm -rf <store>/epics`) or generate into a fresh path.
|
||||||
|
|
||||||
|
## parse_v6_epics.py warnings
|
||||||
|
|
||||||
|
The parser emits warnings in its `warnings[]` array rather than failing. The LLM driving the migrate flow surfaces these to the user for confirmation.
|
||||||
|
|
||||||
|
| Warning shape | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `no '## Requirements Inventory' section found` | The v6 file doesn't have the canonical inventory heading. The migrate flow proceeds without populating the requirements block. |
|
||||||
|
| `no '## Epic N:' headings found; file may not be canonical v6` | The parser couldn't find epic headings. Likely a hand-edited v6 file. Pick option 3 from the migrate menu (walk through manually). |
|
||||||
|
| `epic N (Title): no stories parsed` | An epic heading was found but no `### Story N.M:` blocks under it. Either the v6 file has stories elsewhere, or the epic is genuinely empty. |
|
||||||
|
| `epic N story M: no acceptance criteria parsed` | The `**Acceptance Criteria:**` block is missing or formatted differently. The v7 story will have empty ACs; fix in the LLM-driven migrate confirmation step. |
|
||||||
|
| `input <path> is a directory; sharded v6 input — flatten first` | Sharded v6 detected. The migrate flow offers to flatten before re-parsing. |
|
||||||
|
|
||||||
|
## Common operator scenarios
|
||||||
|
|
||||||
|
### "The skill won't let me edit because it thinks it's create mode"
|
||||||
|
|
||||||
|
Mode detection is filesystem-driven. Either `{initiative_store}/epics/` has no folders matching `NN-*`, or your `{initiative_store}` is pointing at the wrong path. Check `_bmad/config.yaml` and the resolution chain documented in `SKILL.md` Step 4 ("Load Config").
|
||||||
|
|
||||||
|
### "Validation passes but I know there's a coverage gap"
|
||||||
|
|
||||||
|
You forgot to pass `--inventory`. Without it, the validator only emits `mentioned_requirements` (the textual extraction) and won't compare to anything. The coverage gate requires the inventory file to compare against.
|
||||||
|
|
||||||
|
### "I want to delete an epic and the skill won't let me"
|
||||||
|
|
||||||
|
By design — see `prompts/edit-mode.md` under "Boundaries". Run `rm -rf {initiative_store}/epics/<folder>`, then `re-validate` to surface every dep ref that pointed at the deleted epic.
|
||||||
|
|
||||||
|
### "rename_epic.py refuses to renumber"
|
||||||
|
|
||||||
|
Probably the new NN collides with another existing epic. The message says which one. Renumber that epic out of the way first.
|
||||||
|
|
||||||
|
### "I keep getting story-numbering-gaps after a manual delete"
|
||||||
|
|
||||||
|
`rm`'ing a story file leaves a gap (e.g. 01, 02, 04). Fix with `rename_story.py --epic <e> --from <basename> --to-nn N` to renumber the survivors into a contiguous sequence.
|
||||||
|
|
||||||
|
### "Validator output is huge — I just want to see one thing"
|
||||||
|
|
||||||
|
For a quick view of the tree shape: `--summary-only` (JSON) or `--tree` (plain text). Both skip the findings list.
|
||||||
|
|
||||||
|
For a single epic's findings: `--epic <folder>`. Cross-epic refs still resolve against the whole tree, so a story whose dep references another epic gets reported correctly.
|
||||||
|
|
||||||
|
### "validate_initiative.py exits 0 but the JSON has errors"
|
||||||
|
|
||||||
|
The exit code only counts `error`-level findings. Schema and dep checks are always errors; coverage findings default to warnings unless `--coverage-strict`. If you want exit 1 on warnings too, post-process the JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
out=$(python3 scripts/validate_initiative.py --initiative-store $S)
|
||||||
|
warns=$(echo "$out" | jq '.summary.warnings')
|
||||||
|
[ "$warns" -gt 0 ] && exit 1 || exit 0
|
||||||
|
```
|
||||||
|
|
@ -90,7 +90,7 @@ def main() -> int:
|
||||||
f"title: {yaml_quote(args.title)}\n"
|
f"title: {yaml_quote(args.title)}\n"
|
||||||
f"type: {args.type}\n"
|
f"type: {args.type}\n"
|
||||||
"status: draft\n"
|
"status: draft\n"
|
||||||
f"epic: {yaml_quote(args.epic)}\n"
|
f"epic: {args.epic}\n"
|
||||||
f"depends_on: {deps_yaml}\n"
|
f"depends_on: {deps_yaml}\n"
|
||||||
"---\n\n"
|
"---\n\n"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ def main() -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
text = src_path.read_text(encoding="utf-8")
|
text = src_path.read_text(encoding="utf-8")
|
||||||
text = re.sub(r"^epic:.*$", f"epic: {yaml_quote(args.to_epic)}", text, count=1, flags=re.MULTILINE)
|
text = re.sub(r"^epic:.*$", f"epic: {args.to_epic}", text, count=1, flags=re.MULTILINE)
|
||||||
# The moved story's own depends_on may carry bare basenames that referenced
|
# The moved story's own depends_on may carry bare basenames that referenced
|
||||||
# within-epic siblings in src_epic; those refs now need cross-epic form.
|
# within-epic siblings in src_epic; those refs now need cross-epic form.
|
||||||
new_text_lines: list[str] = []
|
new_text_lines: list[str] = []
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ USER_STORY_RE = re.compile(
|
||||||
)
|
)
|
||||||
AC_RE = re.compile(r"\*\*Acceptance Criteria\*\*:?\s*\n+", re.IGNORECASE)
|
AC_RE = re.compile(r"\*\*Acceptance Criteria\*\*:?\s*\n+", re.IGNORECASE)
|
||||||
GIVEN_WHEN_THEN_RE = re.compile(r"^[-*]\s+(?:Given|When|Then|And|But)\b.*$", re.MULTILINE | re.IGNORECASE)
|
GIVEN_WHEN_THEN_RE = re.compile(r"^[-*]\s+(?:Given|When|Then|And|But)\b.*$", re.MULTILINE | re.IGNORECASE)
|
||||||
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR)\d+(?:\.\d+)?\b")
|
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR|D|R)\d+(?:\.\d+)?\b")
|
||||||
REQUIREMENT_LINE_RE = re.compile(r"^[-*]\s+(?:\*\*)?(FR\d+(?:\.\d+)?|NFR\d+(?:\.\d+)?|UX-DR\d+(?:\.\d+)?)(?:\*\*)?:?\s*(.*)$", re.MULTILINE)
|
REQUIREMENT_LINE_RE = re.compile(r"^[-*]\s+(?:\*\*)?(FR\d+(?:\.\d+)?|NFR\d+(?:\.\d+)?|UX-DR\d+(?:\.\d+)?)(?:\*\*)?:?\s*(.*)$", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ def main() -> int:
|
||||||
if sf.name == "epic.md":
|
if sf.name == "epic.md":
|
||||||
continue
|
continue
|
||||||
t = sf.read_text(encoding="utf-8")
|
t = sf.read_text(encoding="utf-8")
|
||||||
new = re.sub(r"^epic:.*$", f"epic: {yaml_quote(new_folder)}", t, count=1, flags=re.MULTILINE)
|
new = re.sub(r"^epic:.*$", f"epic: {new_folder}", t, count=1, flags=re.MULTILINE)
|
||||||
if new != t:
|
if new != t:
|
||||||
sf.write_text(new, encoding="utf-8")
|
sf.write_text(new, encoding="utf-8")
|
||||||
refs_updated += 1
|
refs_updated += 1
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ class TestInitStory(unittest.TestCase):
|
||||||
content = Path(json.loads(r.stdout)["path"]).read_text(encoding="utf-8")
|
content = Path(json.loads(r.stdout)["path"]).read_text(encoding="utf-8")
|
||||||
self.assertIn("As a {{user_type}}", content)
|
self.assertIn("As a {{user_type}}", content)
|
||||||
self.assertIn("type: feature", content)
|
self.assertIn("type: feature", content)
|
||||||
self.assertIn(f'epic: "{epic}"', content)
|
self.assertIn(f"epic: {epic}", content)
|
||||||
|
self.assertNotIn(f'epic: "{epic}"', content)
|
||||||
self.assertIn("status: draft", content)
|
self.assertIn("status: draft", content)
|
||||||
# The user-story marker comments stay so the LLM can locate the block.
|
# The user-story marker comments stay so the LLM can locate the block.
|
||||||
self.assertIn("USER_STORY_START", content)
|
self.assertIn("USER_STORY_START", content)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class TestMoveStory(unittest.TestCase):
|
||||||
data = json.loads(r.stdout)
|
data = json.loads(r.stdout)
|
||||||
self.assertEqual(data["new"], "02-mig/01-register")
|
self.assertEqual(data["new"], "02-mig/01-register")
|
||||||
moved = (store / "epics" / "02-mig" / "01-register.md").read_text(encoding="utf-8")
|
moved = (store / "epics" / "02-mig" / "01-register.md").read_text(encoding="utf-8")
|
||||||
self.assertIn('epic: "02-mig"', moved)
|
self.assertIn("epic: 02-mig", moved)
|
||||||
self.assertIn('"01-auth/01-schema"', moved)
|
self.assertIn('"01-auth/01-schema"', moved)
|
||||||
self.assertNotIn('depends_on: ["01-schema"]', moved)
|
self.assertNotIn('depends_on: ["01-schema"]', moved)
|
||||||
self.assertFalse((store / "epics" / "01-auth" / "02-register.md").exists())
|
self.assertFalse((store / "epics" / "01-auth" / "02-register.md").exists())
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class TestRenameEpic(unittest.TestCase):
|
||||||
self.assertEqual(data["new"], "01-user-authentication")
|
self.assertEqual(data["new"], "01-user-authentication")
|
||||||
self.assertTrue((store / "epics" / "01-user-authentication" / "epic.md").is_file())
|
self.assertTrue((store / "epics" / "01-user-authentication" / "epic.md").is_file())
|
||||||
schema = (store / "epics" / "01-user-authentication" / "01-schema.md").read_text(encoding="utf-8")
|
schema = (store / "epics" / "01-user-authentication" / "01-schema.md").read_text(encoding="utf-8")
|
||||||
self.assertIn('epic: "01-user-authentication"', schema)
|
self.assertIn("epic: 01-user-authentication", schema)
|
||||||
mailer = (store / "epics" / "02-migration" / "01-mailer.md").read_text(encoding="utf-8")
|
mailer = (store / "epics" / "02-migration" / "01-mailer.md").read_text(encoding="utf-8")
|
||||||
self.assertIn('"01-user-authentication/01-schema"', mailer)
|
self.assertIn('"01-user-authentication/01-schema"', mailer)
|
||||||
v = _run(VALIDATE, "--initiative-store", str(store))
|
v = _run(VALIDATE, "--initiative-store", str(store))
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,33 @@ class TestValidateInitiative(unittest.TestCase):
|
||||||
errors = [f for f in data["findings"] if f["code"] == "coverage-missing"]
|
errors = [f for f in data["findings"] if f["code"] == "coverage-missing"]
|
||||||
self.assertEqual(errors[0]["level"], "error")
|
self.assertEqual(errors[0]["level"], "error")
|
||||||
|
|
||||||
|
def test_inventory_coverage_recognizes_debt_and_research_codes(self) -> None:
|
||||||
|
# The validator's mentioned-codes regex must include D (debt) and R (research)
|
||||||
|
# in addition to FR/NFR/UX-DR. Otherwise tech-debt and research initiatives
|
||||||
|
# always report spurious coverage-missing on inventory codes the stories
|
||||||
|
# actually reference.
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
store = Path(tmp)
|
||||||
|
_build_clean_tree(store)
|
||||||
|
inv = store / "inventory.json"
|
||||||
|
inv.write_text(json.dumps({
|
||||||
|
"requirements": {
|
||||||
|
"debt": [{"code": "D1", "text": "Remove legacy /charge endpoint"}],
|
||||||
|
"research": [{"code": "R1", "text": "Investigate webhook ordering"}],
|
||||||
|
}
|
||||||
|
}), encoding="utf-8")
|
||||||
|
schema = store / "epics" / "01-auth" / "01-schema.md"
|
||||||
|
schema.write_text(
|
||||||
|
schema.read_text(encoding="utf-8") + "\n## Coverage\n- AC1: D1, R1\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
r = _run(VALIDATE, "--initiative-store", str(store), "--inventory", str(inv))
|
||||||
|
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||||
|
data = json.loads(r.stdout)
|
||||||
|
self.assertEqual(data["summary"]["coverage_missing"], [])
|
||||||
|
self.assertIn("D1", data["summary"]["mentioned_requirements"])
|
||||||
|
self.assertIn("R1", data["summary"]["mentioned_requirements"])
|
||||||
|
|
||||||
def test_summary_only_emits_per_story_metadata(self) -> None:
|
def test_summary_only_emits_per_story_metadata(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
store = Path(tmp)
|
store = Path(tmp)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ STORY_REQUIRED = {"title", "type", "status", "epic", "depends_on"}
|
||||||
EPIC_KEYS = {"title", "epic", "status", "depends_on", "metadata"}
|
EPIC_KEYS = {"title", "epic", "status", "depends_on", "metadata"}
|
||||||
EPIC_REQUIRED = {"title", "epic", "status", "depends_on"}
|
EPIC_REQUIRED = {"title", "epic", "status", "depends_on"}
|
||||||
|
|
||||||
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR)\d+(?:\.\d+)?\b")
|
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR|D|R)\d+(?:\.\d+)?\b")
|
||||||
|
|
||||||
|
|
||||||
def parse_frontmatter(text: str) -> tuple[dict | None, str | None]:
|
def parse_frontmatter(text: str) -> tuple[dict | None, str | None]:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue