anthropic critique of skill adjustments

This commit is contained in:
pbean 2026-04-30 16:02:43 -07:00
parent dd1efb3c61
commit 1136a59e0c
17 changed files with 196 additions and 39 deletions

View File

@ -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 13 skipped entirely. A structured spec drives Stages 4 and 5 deterministically. Right for pipelines and pre-drafted plans. - **From-spec** (`--from-spec <path>`) — Stages 13 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 13, 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 13, 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

View File

@ -8,7 +8,7 @@
## Pre-flight ## Pre-flight
Before launching the artifact-analyzer, tell the user (in 35 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 35 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 48 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 48 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.

View File

@ -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.

View File

@ -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

View File

@ -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`).

View File

@ -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`.

View File

@ -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`.

View File

@ -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
```

View File

@ -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"
) )

View File

@ -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] = []

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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())

View File

@ -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))

View File

@ -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)

View File

@ -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]: