doccraft — planning stories
When to use
- Creating a new story in
{{DOCS_DIR}}/stories/.
- Updating status, acceptance criteria, tags, or
openspec on an existing story.
- When closing a story: update
{{DOCS_DIR}}/queue.md and the Status
column in {{DOCS_DIR}}/backlog.md. See Workflow reminders for
queue-audit invocation rules.
YAML frontmatter (required fields)
Use valid YAML between --- delimiters at the top of the file. Every
field marked yes MUST be present — do not omit any. If the user has
not provided enough information to fill a required field, ask — NEVER guess.
| Field |
Required |
Values / notes |
id |
yes |
Stable, unique id across all stories. P0.3 when aligned to {{DOCS_DIR}}/backlog.md, or a slug like story-2026-001. MUST be unique — verify before writing. |
title |
yes |
Short human-readable title. |
status |
yes |
One of the values in story.status config (default: todo | in_progress | done). Manual updates only. |
impact |
yes |
One of the values in story.impact config (default: H | M | L). If the user does not specify, ask. |
urgency |
yes |
One of the values in story.urgency config (default: now | soon | later). If the user does not specify, ask. |
tags |
yes |
YAML list of prefixed strings (area:, slice:, theme:) from the tag vocabulary below. If nothing fits and the label will recur, extend the vocabulary in doccraft.yaml in the same change. |
openspec |
yes |
MUST be set to one of: not-needed | recommended | required. See openspec guidance below. |
updated |
yes |
ISO date YYYY-MM-DD. MUST be set on creation and updated on every meaningful edit. |
roadmap_ref |
optional |
e.g. P1.7 — pointer to the backlog row when applicable. |
depends_on |
optional |
YAML list of story id values that MUST be satisfied before this story is picked up (prerequisites). Omit or [] if none. Each entry MUST match another story's id or a backlog id you intentionally treat as external — prefer real story ids so the queue-audit graph stays honest. |
adr_refs |
optional |
List of ADR filenames this story implements or contradicts (e.g. 001-foo.md). |
openspec_change |
optional |
Path or name of the OpenSpec change folder when one exists. |
Do not invent new values for status, impact, or urgency without first
adding them to the matching story.* enum in doccraft.json — those are
the single source of truth. For openspec, update this skill in the same
change. One-off nuance belongs in the body, not as a new enum value.
openspec guidance
not-needed — small change, obvious scope, few files.
recommended — multi-module, schema/graph shifts, ambiguous scope, or
high regression risk; add a sentence in the body:
OpenSpec recommended because: …
required — project policy demands formal spec-before-code for this
class of change.
Do not create an openspec/ tree unless the repository has adopted OpenSpec;
the field is preparatory.
File location and naming
- Path:
{{DOCS_DIR}}/stories/<slug>.md — kebab-case slug.
- P-tier stories (aligned with a prioritised backlog): use
p{tier}-<topic>.md where tier is p0…p4. The ordinal (e.g. P0.3)
lives in YAML id, not in the filename. Examples:
p0-payment-retry-flow.md, p2-observability-rollout.md.
- Non-tier work: stable prefix + slug, e.g.
opt-2a-workflow-rename.md.
- One story per file. No epic folders — use prefixed
tags for grouping.
Body template
After frontmatter, use these markdown sections in this order:
- Problem / outcome (required) — what user or system need this addresses.
- Acceptance criteria (required) — bullet list, testable where possible.
- Notes (optional) — links to code (
src/...), related ADRs, PRs.
Do not add other top-level sections. Put additional context in Notes.
Example
---
id: P0.3
title: Payment retry flow with idempotency keys
status: todo
impact: H
urgency: now
tags:
- area:api
- area:data
openspec: recommended
updated: 2026-04-18
roadmap_ref: P0.3
depends_on: []
adr_refs:
- 003-payment-gateway-choice.md
---
## Problem / outcome
Failed third-party charges silently drop transactions; add retries with
idempotency so users can reorder without double-billing.
## Acceptance criteria
- [ ] Retries use persisted idempotency keys.
- [ ] Integration tests cover success, transient-failure, and permanent-failure paths.
- [ ] Runbook updated with the new retry behaviour.
## Notes
OpenSpec recommended because: touches schema + payment service + integration tests.
Tag vocabulary
Every tag MUST use a prefix so subsystem vs product slice vs cross-cutting
theme is unambiguous. Use lowercase after the colon (e.g. area:api).
| Prefix |
Meaning |
Examples |
area: |
Subsystem / code area. Align with your project's commit scopes where you already have them. |
area:api, area:cli, area:auth, area:data, area:infra, area:schemas |
slice: |
Product surface that spans multiple areas. |
slice:ui, slice:admin, slice:onboarding |
theme: |
Cross-cutting quality or kind of work. |
theme:observability, theme:performance, theme:security, theme:docs, theme:testing |
A story may list several tags, e.g. area:api, area:data, theme:performance.
Extending the vocabulary
- If no existing
area:, slice:, or theme: value fits, and the
label will recur, add it to the matching list in doccraft.yaml
(keys story.areas / story.slices / story.themes). Commit the
config edit together with the first story that uses the new value.
- One-off nuance that will not recur belongs in the body (Notes),
not as a new tag.
Do not edit the tables in this SKILL.md directly — doccraft update
regenerates this file and would overwrite the edit. doccraft.yaml
is the single source of truth for project-specific vocabulary.
Invalid examples (do not use)
- Bare words:
api, ui — always use a prefix so the kind of label is
explicit.
- Wrong prefix for the kind of label (e.g.
area:ui while slice:ui is the
convention) — prefer slice: for product surfaces.
Pre-write validation
Before writing or updating a story file, MUST complete these checks:
- Unique
id — scan all {{DOCS_DIR}}/stories/*.md frontmatter and
confirm the id value does not already exist. If it does, stop and ask
the user for a different id.
- Valid
depends_on — every entry in depends_on MUST match an
existing story's id value. If a reference is not found, stop and ask
the user to clarify.
- Valid tags — every tag MUST use a recognised prefix (
area:,
slice:, theme:). Bare words are NEVER acceptable.
- Valid enums —
status, impact, urgency, and openspec values
MUST match the allowed values (from doccraft.yaml or the defaults in
the frontmatter table above).
- Required fields present — all fields marked "yes" in the frontmatter
table MUST be present. If the user has not provided enough information
to fill
impact, urgency, or openspec, ask — do not guess.
Done condition
The task is complete when:
- The story file exists at the correct path (
{{DOCS_DIR}}/stories/<slug>.md).
- Frontmatter contains all required fields with valid values.
- Body contains at minimum Problem / outcome and Acceptance criteria.
- If
depends_on was added or changed, doccraft-queue-audit has been
invoked in the same turn.
- If the story was closed (
status: done), {{DOCS_DIR}}/queue.md and
{{DOCS_DIR}}/backlog.md have been updated.
Configuration
Read doccraft.yaml at invocation. The story: section is this
skill's customisation surface; override the defaults in the tables above
with the values found there. If the file is missing or the story:
section is absent, use the defaults as-is.
Relevant keys:
docsDir — root folder for all docs, relative to project root. Default:
docs. Stories live at {docsDir}/stories/.
story.areas, story.slices, story.themes — tag vocabulary lists
(replace the default area: / slice: / theme: values).
story.status — allowed values for the status: field. Default:
[todo, in_progress, done]. Extend if your project uses additional
states (e.g. blocked, abandoned).
story.urgency — allowed values for the urgency: field. Default:
[now, soon, later]. Some projects use tier names (p0..p4) directly
here, or mix both.
story.impact — allowed values for the impact: field. Default:
[H, M, L]. Override for projects that prefer [high, medium, low]
or another taxonomy.
story.id.tiers — filename tier prefixes like p0…p4. Empty list
[] means the project does not use tier prefixes.
story.id.pattern — regex accepting valid story id: values in
frontmatter. Use this to validate new stories and to normalise
depends_on typos. Default: ^(P\d+(\.\d+)?|[a-z][a-z0-9-]+)$.
Adding to a list in doccraft.yaml teaches the skill a new valid
value without touching this file (which doccraft update regenerates).
That is the intended way to extend vocabulary for a project.
Package context
For monorepo projects, stories may live at the project root or under a
declared package's {{DOCS_DIR}}/stories/ tree (a Known package roots
block appears above when packages are declared). For single-root projects
no block appears, every story lives at the project-root
{{DOCS_DIR}}/stories/, and the namespace rules below do not apply.
When the project declares packages, decide which scope to write to:
- Explicit
package: arg. If the user names a package (e.g. "create
a story for audio-engine"), write to that package's
{{DOCS_DIR}}/stories/ and use the namespaced id form
<slug>/STR-NNNN.
- Active-file inference. If the user is editing a file under a
declared package path, default to that package's docs root.
- Root default. Otherwise, write to the project-root
{{DOCS_DIR}}/stories/ with an unprefixed id.
In depends_on, the form <slug>/STR-NNNN references a story under that
package's {{DOCS_DIR}}/stories/; unprefixed ids always refer to the
project-root scope. The same namespace rule applies to adr_refs
(unprefixed = root, <slug>/NNN-slug.md = package-scoped).
Cross-reference resolution (path-bearing links)
Per ADR 017, a cross-reference should resolve in one hop — opening the
target should never require globbing a directory to find the file. The
namespaced id stays the canonical, rename-stable handle; pair it with a path
so neither readers nor agents pay a search tax:
- In frontmatter lists (
depends_on, adr_refs), keep the id form —
link syntax in a YAML list is awkward, and the audit resolves these against
the Known package roots anyway. Ids remain canonical here.
- In prose, tables, and Notes — whenever you write a reference a reader
or agent will follow (a related-story line, a queue row, an epic table),
render it as a markdown link whose text is the id and whose target is the
resolvable file path:
[pipeline/P1.30](../services/pipeline/{{DOCS_DIR}}/stories/p1-...md).
- The id is the source of truth; the path is a convenience that the audit
(
doccraft-queue-audit) re-derives from the id and repairs when a file
moves — so a stale path is a mechanical fix, not a dead link.
Workflow reminders
- Move
status to in_progress when you start implementation; done when
shipped or explicitly abandoned (note why in body if abandoned).
- After creating a story or changing
depends_on, invoke
doccraft-queue-audit in the same turn so the working queue stays
consistent with the dependency graph.
- When closing a story (
status: done), update {{DOCS_DIR}}/queue.md
and the Status column in {{DOCS_DIR}}/backlog.md in the same commit.
1---2name: doccraft-story-23description: Author or update product stories (a.k.a. planning docs, backlog items, tickets, specs) as Markdown under {{DOCS_DIR}}/stories/ with a YAML frontmatter contract (id, status, impact, urgency, depends_on, tags, openspec). Use this whenever the user is creating a story, reprioritising work, writing acceptance criteria, linking a story to OpenSpec or an ADR, or editing anything under {{DOCS_DIR}}/stories/ — even if they call it a spec, ticket, backlog row, or planning doc.4---56# doccraft — planning stories78## When to use910- Creating a new story in `{{DOCS_DIR}}/stories/`.11- Updating status, acceptance criteria, tags, or `openspec` on an existing story.12- When closing a story: update `{{DOCS_DIR}}/queue.md` and the **Status**13 column in `{{DOCS_DIR}}/backlog.md`. See **Workflow reminders** for14 queue-audit invocation rules.1516## YAML frontmatter (required fields)1718Use valid YAML between `---` delimiters at the top of the file. Every19field marked **yes** MUST be present — do not omit any. If the user has20not provided enough information to fill a required field, ask — NEVER guess.2122| Field | Required | Values / notes |23|-------|----------|----------------|24| `id` | yes | Stable, unique id across all stories. `P0.3` when aligned to `{{DOCS_DIR}}/backlog.md`, or a slug like `story-2026-001`. MUST be unique — verify before writing. |25| `title` | yes | Short human-readable title. |26| `status` | yes | One of the values in `story.status` config (default: `todo` \| `in_progress` \| `done`). **Manual** updates only. |27| `impact` | yes | One of the values in `story.impact` config (default: `H` \| `M` \| `L`). If the user does not specify, ask. |28| `urgency` | yes | One of the values in `story.urgency` config (default: `now` \| `soon` \| `later`). If the user does not specify, ask. |29| `tags` | yes | YAML list of **prefixed** strings (`area:`, `slice:`, `theme:`) from the tag vocabulary below. If nothing fits and the label will recur, extend the vocabulary in `doccraft.yaml` in the same change. |30| `openspec` | yes | MUST be set to one of: `not-needed` \| `recommended` \| `required`. See `openspec` guidance below. |31| `updated` | yes | ISO date `YYYY-MM-DD`. MUST be set on creation and updated on every meaningful edit. |32| `roadmap_ref` | optional | e.g. `P1.7` — pointer to the backlog row when applicable. |33| `depends_on` | optional | YAML list of **story `id` values** that MUST be satisfied **before** this story is picked up (prerequisites). Omit or `[]` if none. Each entry MUST match another story's `id` or a backlog id you intentionally treat as external — prefer real story ids so the queue-audit graph stays honest. |34| `adr_refs` | optional | List of ADR filenames this story implements or contradicts (e.g. `001-foo.md`). |35| `openspec_change` | optional | Path or name of the OpenSpec change folder when one exists. |3637> Do not invent new values for `status`, `impact`, or `urgency` without first38> adding them to the matching `story.*` enum in `doccraft.json` — those are39> the single source of truth. For `openspec`, update this skill in the same40> change. One-off nuance belongs in the body, not as a new enum value.4142<!-- doccraft:inject point=story.frontmatter.fields -->43<!-- /doccraft:inject -->4445### `openspec` guidance4647- **`not-needed`** — small change, obvious scope, few files.48- **`recommended`** — multi-module, schema/graph shifts, ambiguous scope, or49 high regression risk; add a sentence in the body:50 *OpenSpec recommended because: …*51- **`required`** — project policy demands formal spec-before-code for this52 class of change.5354Do not create an `openspec/` tree unless the repository has adopted OpenSpec;55the field is preparatory.5657## File location and naming5859- Path: **`{{DOCS_DIR}}/stories/<slug>.md`** — kebab-case slug.60- **P-tier stories** (aligned with a prioritised backlog): use61 `p{tier}-<topic>.md` where tier is `p0`…`p4`. The ordinal (e.g. `P0.3`)62 lives in YAML `id`, not in the filename. Examples:63 `p0-payment-retry-flow.md`, `p2-observability-rollout.md`.64- **Non-tier work**: stable prefix + slug, e.g. `opt-2a-workflow-rename.md`.65- One story per file. **No epic folders** — use prefixed `tags` for grouping.6667## Body template6869After frontmatter, use these markdown sections in this order:70711. **Problem / outcome** *(required)* — what user or system need this addresses.722. **Acceptance criteria** *(required)* — bullet list, testable where possible.733. **Notes** *(optional)* — links to code (`src/...`), related ADRs, PRs.7475Do not add other top-level sections. Put additional context in **Notes**.7677<!-- doccraft:inject point=story.body.sections -->78<!-- /doccraft:inject -->7980## Example8182````markdown83---84id: P0.385title: Payment retry flow with idempotency keys86status: todo87impact: H88urgency: now89tags:90 - area:api91 - area:data92openspec: recommended93updated: 2026-04-1894roadmap_ref: P0.395depends_on: []96adr_refs:97 - 003-payment-gateway-choice.md98---99100## Problem / outcome101102Failed third-party charges silently drop transactions; add retries with103idempotency so users can reorder without double-billing.104105## Acceptance criteria106107- [ ] Retries use persisted idempotency keys.108- [ ] Integration tests cover success, transient-failure, and permanent-failure paths.109- [ ] Runbook updated with the new retry behaviour.110111## Notes112113OpenSpec recommended because: touches schema + payment service + integration tests.114````115116## Tag vocabulary117118Every tag MUST use a **prefix** so subsystem vs product slice vs cross-cutting119theme is unambiguous. Use **lowercase** after the colon (e.g. `area:api`).120121| Prefix | Meaning | Examples |122|--------|---------|----------|123| `area:` | Subsystem / code area. Align with your project's commit scopes where you already have them. | `area:api`, `area:cli`, `area:auth`, `area:data`, `area:infra`, `area:schemas` |124| `slice:` | Product surface that spans multiple areas. | `slice:ui`, `slice:admin`, `slice:onboarding` |125| `theme:` | Cross-cutting quality or kind of work. | `theme:observability`, `theme:performance`, `theme:security`, `theme:docs`, `theme:testing` |126127A story may list several tags, e.g. `area:api`, `area:data`, `theme:performance`.128129### Extending the vocabulary130131- If **no** existing `area:`, `slice:`, or `theme:` value fits, and the132 label will **recur**, add it to the matching list in `doccraft.yaml`133 (keys `story.areas` / `story.slices` / `story.themes`). Commit the134 config edit together with the first story that uses the new value.135- **One-off** nuance that will not recur belongs in the body (**Notes**),136 not as a new tag.137138Do not edit the tables in this `SKILL.md` directly — `doccraft update`139regenerates this file and would overwrite the edit. `doccraft.yaml`140is the single source of truth for project-specific vocabulary.141142### Invalid examples (do not use)143144- Bare words: `api`, `ui` — always use a prefix so the kind of label is145 explicit.146- Wrong prefix for the kind of label (e.g. `area:ui` while `slice:ui` is the147 convention) — prefer `slice:` for product surfaces.148149## Pre-write validation150151Before writing or updating a story file, MUST complete these checks:1521531. **Unique `id`** — scan all `{{DOCS_DIR}}/stories/*.md` frontmatter and154 confirm the `id` value does not already exist. If it does, stop and ask155 the user for a different id.1562. **Valid `depends_on`** — every entry in `depends_on` MUST match an157 existing story's `id` value. If a reference is not found, stop and ask158 the user to clarify.1593. **Valid tags** — every tag MUST use a recognised prefix (`area:`,160 `slice:`, `theme:`). Bare words are NEVER acceptable.1614. **Valid enums** — `status`, `impact`, `urgency`, and `openspec` values162 MUST match the allowed values (from `doccraft.yaml` or the defaults in163 the frontmatter table above).1645. **Required fields present** — all fields marked "yes" in the frontmatter165 table MUST be present. If the user has not provided enough information166 to fill `impact`, `urgency`, or `openspec`, ask — do not guess.167168## Done condition169170The task is complete when:171172- The story file exists at the correct path (`{{DOCS_DIR}}/stories/<slug>.md`).173- Frontmatter contains all required fields with valid values.174- Body contains at minimum **Problem / outcome** and **Acceptance criteria**.175- If `depends_on` was added or changed, `doccraft-queue-audit` has been176 invoked in the same turn.177- If the story was closed (`status: done`), `{{DOCS_DIR}}/queue.md` and178 `{{DOCS_DIR}}/backlog.md` have been updated.179180## Configuration181182Read `doccraft.yaml` at invocation. The `story:` section is this183skill's customisation surface; override the defaults in the tables above184with the values found there. If the file is missing or the `story:`185section is absent, use the defaults as-is.186187Relevant keys:188189- `docsDir` — root folder for all docs, relative to project root. Default:190 `docs`. Stories live at `{docsDir}/stories/`.191- `story.areas`, `story.slices`, `story.themes` — tag vocabulary lists192 (replace the default `area:` / `slice:` / `theme:` values).193- `story.status` — allowed values for the `status:` field. Default:194 `[todo, in_progress, done]`. Extend if your project uses additional195 states (e.g. `blocked`, `abandoned`).196- `story.urgency` — allowed values for the `urgency:` field. Default:197 `[now, soon, later]`. Some projects use tier names (`p0..p4`) directly198 here, or mix both.199- `story.impact` — allowed values for the `impact:` field. Default:200 `[H, M, L]`. Override for projects that prefer `[high, medium, low]`201 or another taxonomy.202- `story.id.tiers` — filename tier prefixes like `p0`…`p4`. Empty list203 `[]` means the project does not use tier prefixes.204- `story.id.pattern` — regex accepting valid story `id:` values in205 frontmatter. Use this to validate new stories and to normalise206 `depends_on` typos. Default: `^(P\d+(\.\d+)?|[a-z][a-z0-9-]+)$`.207208Adding to a list in `doccraft.yaml` teaches the skill a new valid209value without touching this file (which `doccraft update` regenerates).210That is the intended way to extend vocabulary for a project.211212## Package context213214<!-- doccraft:packages -->215<!-- /doccraft:packages -->216217For monorepo projects, stories may live at the project root or under a218declared package's `{{DOCS_DIR}}/stories/` tree (a **Known package roots**219block appears above when packages are declared). For single-root projects220no block appears, every story lives at the project-root221`{{DOCS_DIR}}/stories/`, and the namespace rules below do not apply.222223When the project declares packages, decide which scope to write to:224225- **Explicit `package:` arg.** If the user names a package (e.g. "create226 a story for `audio-engine`"), write to that package's227 `{{DOCS_DIR}}/stories/` and use the namespaced id form228 `<slug>/STR-NNNN`.229- **Active-file inference.** If the user is editing a file under a230 declared package path, default to that package's docs root.231- **Root default.** Otherwise, write to the project-root232 `{{DOCS_DIR}}/stories/` with an unprefixed id.233234In `depends_on`, the form `<slug>/STR-NNNN` references a story under that235package's `{{DOCS_DIR}}/stories/`; unprefixed ids always refer to the236project-root scope. The same namespace rule applies to `adr_refs`237(unprefixed = root, `<slug>/NNN-slug.md` = package-scoped).238239## Cross-reference resolution (path-bearing links)240241Per ADR 017, a cross-reference should resolve in **one hop** — opening the242target should never require globbing a directory to find the file. The243namespaced id stays the canonical, rename-stable handle; pair it with a path244so neither readers nor agents pay a search tax:245246- **In frontmatter lists** (`depends_on`, `adr_refs`), keep the id form —247 link syntax in a YAML list is awkward, and the audit resolves these against248 the **Known package roots** anyway. Ids remain canonical here.249- **In prose, tables, and Notes** — whenever you *write* a reference a reader250 or agent will follow (a related-story line, a queue row, an epic table),251 render it as a markdown link whose text is the id and whose target is the252 resolvable file path: `[pipeline/P1.30](../services/pipeline/{{DOCS_DIR}}/stories/p1-...md)`.253- The id is the source of truth; the path is a convenience that the audit254 (`doccraft-queue-audit`) re-derives from the id and repairs when a file255 moves — so a stale path is a mechanical fix, not a dead link.256257## Workflow reminders258259- Move `status` to `in_progress` when you start implementation; `done` when260 shipped or explicitly abandoned (note why in body if abandoned).261- After creating a story or changing `depends_on`, invoke262 `doccraft-queue-audit` in the same turn so the working queue stays263 consistent with the dependency graph.264- When closing a story (`status: done`), update `{{DOCS_DIR}}/queue.md`265 and the **Status** column in `{{DOCS_DIR}}/backlog.md` in the same commit.266267<!-- doccraft:inject point=story.instructions -->268<!-- /doccraft:inject -->