kataster 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/THIRD_PARTY_NOTICES.md +69 -0
- package/.claude/agents/acceptance-tester.md +7 -2
- package/.claude/agents/code-reviewer.md +7 -2
- package/.claude/agents/ux-designer.md +43 -38
- package/.claude/hooks/validate-docs.ts +2 -1
- package/.claude/lib/config.ts +1 -0
- package/.claude/lib/discovery.ts +129 -31
- package/.claude/lib/engineering.ts +12 -6
- package/.claude/package.json +1 -0
- package/.claude/settings.json +1 -2
- package/.claude/skills/adopt/SKILL.md +5 -3
- package/.claude/skills/backlog/SKILL.md +2 -2
- package/.claude/skills/bootstrap/SKILL.md +9 -7
- package/.claude/skills/domain-model/SKILL.md +1 -1
- package/.claude/skills/feature/SKILL.md +37 -11
- package/.claude/skills/feature/scripts/intake.ts +1 -1
- package/.claude/skills/groom-backlog/SKILL.md +19 -16
- package/.claude/skills/implement/SKILL.md +169 -63
- package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
- package/.claude/skills/implement/scripts/story-context.ts +1 -1
- package/.claude/skills/implement/scripts/verify.ts +1 -1
- package/.claude/skills/prototype/LOGIC.md +101 -0
- package/.claude/skills/prototype/SKILL.md +85 -0
- package/.claude/skills/prototype/UI.md +130 -0
- package/.claude/skills/review-stories/SKILL.md +39 -19
- package/.claude/skills/shape/SKILL.md +2 -2
- package/.claude/skills/user-stories/SKILL.md +109 -51
- package/.claude/skills/user-stories/schema/story.schema.json +6 -3
- package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
- package/.claude/skills/user-stories/templates/story-template.md +5 -2
- package/.claude/skills/ux-design/SKILL.md +30 -5
- package/.opencode/agents/acceptance-tester.md +8 -3
- package/.opencode/agents/code-reviewer.md +8 -3
- package/.opencode/agents/ux-designer.md +44 -39
- package/.opencode/commands/bootstrap.md +1 -1
- package/.opencode/commands/feature.md +1 -1
- package/.opencode/commands/groom-backlog.md +1 -1
- package/.opencode/commands/implement.md +1 -1
- package/.opencode/commands/review-stories.md +1 -1
- package/AGENTS.md +17 -0
- package/CHANGELOG.md +38 -0
- package/README.md +64 -7
- package/guide/brownfield.md +2 -2
- package/guide/concepts.md +26 -5
- package/guide/greenfield.md +4 -3
- package/guide/release.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +42 -4
- package/src/install.mjs +62 -6
- package/templates/docs/product/ux-guidelines.md +2 -0
- package/templates/docs/prototypes/README.md +9 -0
- package/templates/docs/stories/README.md +10 -3
- package/templates/docs/workflow.md +38 -13
- package/.claude/agents/lead-dev.md +0 -50
- package/.claude/agents/product-owner.md +0 -39
- package/.claude/agents/requirements-engineer.md +0 -44
- package/.opencode/agents/lead-dev.md +0 -67
- package/.opencode/agents/product-owner.md +0 -59
- package/.opencode/agents/requirements-engineer.md +0 -58
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Kataster's workflow is partly derived from other people's work. This file says what came from where, so the credit
|
|
4
|
+
travels with the payload: it is copied into every installation together with the skills.
|
|
5
|
+
|
|
6
|
+
Everything not listed here is original to Kataster.
|
|
7
|
+
|
|
8
|
+
## Matt Pocock – Skills for Real Engineers
|
|
9
|
+
|
|
10
|
+
- Upstream: <https://github.com/mattpocock/skills>
|
|
11
|
+
- Site: <https://www.aihero.dev/skills>
|
|
12
|
+
- License: MIT, Copyright (c) 2026 Matt Pocock (full text below)
|
|
13
|
+
|
|
14
|
+
The grilling interview, the deep-module vocabulary, the test-first loop, the architecture survey, the throwaway
|
|
15
|
+
prototype and the `writing-for-agents` reference are his. Some files are copied verbatim, others are adapted to
|
|
16
|
+
Kataster's artifacts (`CONTEXT.md` instead of `GLOSSARY.md`, the `skill` tool name in OpenCode, the seam catalog,
|
|
17
|
+
the review agents):
|
|
18
|
+
|
|
19
|
+
| In Kataster | Upstream | State |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `.claude/skills/grilling/SKILL.md` | `skills/productivity/grilling/SKILL.md` | adapted (naming, sub-agent wording) |
|
|
22
|
+
| `.claude/skills/grill-me/SKILL.md` | `skills/productivity/grill-me/SKILL.md` | copied (tool name) |
|
|
23
|
+
| `.claude/skills/grill-with-docs/SKILL.md` | `skills/engineering/grill-with-docs/SKILL.md` | adapted |
|
|
24
|
+
| `.claude/skills/writing-for-agents/SKILL.md` | `skills/productivity/writing-for-agents/SKILL.md` | copied verbatim |
|
|
25
|
+
| `.claude/skills/writing-for-agents/SKILL-MECHANICS.md` | `skills/productivity/writing-for-agents/SKILL-MECHANICS.md` | copied verbatim |
|
|
26
|
+
| `.claude/skills/codebase-design/SKILL.md` | `skills/engineering/codebase-design/SKILL.md` | copied verbatim |
|
|
27
|
+
| `.claude/skills/codebase-design/DEEPENING.md` | `skills/engineering/codebase-design/DEEPENING.md` | copied verbatim |
|
|
28
|
+
| `.claude/skills/codebase-design/DESIGN-IT-TWICE.md` | `skills/engineering/codebase-design/DESIGN-IT-TWICE.md` | adapted |
|
|
29
|
+
| `.claude/skills/tdd/SKILL.md` | `skills/engineering/tdd/SKILL.md` | adapted (glossary, seam catalog) |
|
|
30
|
+
| `.claude/skills/improve-codebase-architecture/SKILL.md` | `skills/engineering/improve-codebase-architecture/SKILL.md` | adapted (glossary, tool name) |
|
|
31
|
+
| `.claude/skills/retro/SKILL.md` | `skills/engineering/retro/SKILL.md` | adapted |
|
|
32
|
+
| `.claude/skills/domain-model/SKILL.md` | `skills/engineering/domain-modeling/SKILL.md` | adapted (contexts, aggregates, data model) |
|
|
33
|
+
| `.claude/skills/prototype/SKILL.md` | `skills/engineering/prototype/SKILL.md` | adapted (standalone HTML in discovery, story/guideline capture, timebox) |
|
|
34
|
+
| `.claude/skills/prototype/UI.md` | `skills/engineering/prototype/UI.md` | adapted (standalone default, in-app exception, Kataster artifacts) |
|
|
35
|
+
| `.claude/skills/prototype/LOGIC.md` | `skills/engineering/prototype/LOGIC.md` | adapted (Kataster artifacts) |
|
|
36
|
+
|
|
37
|
+
Agents and commands built on top of them (`/implement`, `/feature`, `/review-stories`, `/groom-backlog`,
|
|
38
|
+
`/shape`, the review agents in `.claude/agents/`) are Kataster's own, but they stand on the same ideas: grilling as
|
|
39
|
+
a design-tree interview on the frontier, facts looked up rather than asked, decisions left to the user.
|
|
40
|
+
|
|
41
|
+
## Other ideas
|
|
42
|
+
|
|
43
|
+
- **John Ousterhout**, *A Philosophy of Software Design* – "Design It Twice" is the model for the parallel-interface
|
|
44
|
+
exercise in `codebase-design`; depth, seam and the deletion test shape its vocabulary.
|
|
45
|
+
- **Eric Evans**, *Domain-Driven Design* – ubiquitous language, bounded contexts and aggregates shape the domain
|
|
46
|
+
model and the glossary in `CONTEXT.md`.
|
|
47
|
+
- **Martin Fowler** – the smell baseline behind the review agents.
|
|
48
|
+
|
|
49
|
+
## MIT License
|
|
50
|
+
|
|
51
|
+
Copyright (c) 2026 Matt Pocock
|
|
52
|
+
|
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
54
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
55
|
+
in the Software without restriction, including without limitation the rights
|
|
56
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
57
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
58
|
+
furnished to do so, subject to the following conditions:
|
|
59
|
+
|
|
60
|
+
The above copyright notice and this permission notice shall be included in all
|
|
61
|
+
copies or substantial portions of the Software.
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
66
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
67
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
68
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
69
|
+
SOFTWARE.
|
|
@@ -14,7 +14,7 @@ hooks:
|
|
|
14
14
|
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/path-guard.ts" docs/reviews/'
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
You are the acceptance tester of this project. You check whether **one story** is really done from the user's point of view. You report gaps; you never write or change tests, code or stories – in this project tests are written test-first by whoever implements the story.
|
|
17
|
+
You are the acceptance tester of this project. You check whether **one story** is really done from the user's point of view. You report gaps; you never write or change tests, code or stories – in this project tests are written test-first by whoever implements the story. You never create follow-up stories: a gap is either fixed in this story or handed back as an open question.
|
|
18
18
|
You have NO context from the main conversation – the task gives you the story ID, the base ref and, if one exists, the preview deployment URL. Write in the artifact language (`languages.artifacts` in `.claude/project.json`, default English).
|
|
19
19
|
|
|
20
20
|
## Steps
|
|
@@ -46,8 +46,13 @@ Date: YYYY-MM-DD · Tests run: <command> → <passed/failed counts> · Preview:
|
|
|
46
46
|
| Item | Applies | Evidence | OK? |
|
|
47
47
|
|
|
48
48
|
## Edge cases not covered by the story
|
|
49
|
-
| Case | Expected by a rule? (cite) | Recommendation: fix now /
|
|
49
|
+
| Case | Expected by a rule? (cite) | Recommendation: fix now / question |
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
End with a verdict: `accepted`, `accepted with remarks` or `not accepted` (a scenario without a real test, a failing test, or a violated rule). **Your verdict is the acceptance** (user, 2026-10-27): on `accepted` and `accepted with remarks` the story goes to `done` and the user only merges, so don't accept what you would want the user to look at first.
|
|
53
|
+
|
|
54
|
+
## Boundaries
|
|
55
|
+
- You never create, split or revise stories, and never delegate to another agent. An uncovered edge case is a **question** for the developer or a line in the review file – not a new ticket.
|
|
56
|
+
- A story the linter accepted is still built as written: if a scenario itself is wrong, say so; the developer sends the story back to `review` instead of deciding it in code.
|
|
57
|
+
|
|
53
58
|
Reply to the main session with the verdict, the gaps and the file path.
|
|
@@ -15,7 +15,7 @@ hooks:
|
|
|
15
15
|
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/path-guard.ts" docs/reviews/'
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
You are the code reviewer of this project. You review the changes of **one story** against the project's documented decisions and write your findings down. You never change code, tests or stories.
|
|
18
|
+
You are the code reviewer of this project. You review the changes of **one story** against the project's documented decisions and write your findings down. You never change code, tests or stories, and you never create follow-up stories: a finding is either fixed inside this story or handed back as an open question to the developer.
|
|
19
19
|
You have NO context from the main conversation – the task gives you the story ID, the base ref (usually `main`) and the paths you need. Write in the artifact language (`languages.artifacts` in `.claude/project.json`, default English).
|
|
20
20
|
|
|
21
21
|
## Inputs
|
|
@@ -50,7 +50,12 @@ Base: <ref> · Commits: <n> · Date: YYYY-MM-DD
|
|
|
50
50
|
|---|---|---|---|---|---|---|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Severity: **blocker** (violates an ADR, a domain rule, security or a scenario – must be fixed before merge), **major** (should be fixed in this story), **minor** (judgement call, the main session decides), **
|
|
53
|
+
Severity: **blocker** (violates an ADR, a domain rule, security or a scenario – must be fixed before merge), **major** (should be fixed in this story), **minor** (judgement call, the main session decides), **question** (needs a decision the developer must make – no story, no ticket, just the question and the evidence).
|
|
54
54
|
End with a two-line verdict: `ready to merge` / `fix blockers first`, and the single most important finding.
|
|
55
55
|
|
|
56
|
+
## Boundaries
|
|
57
|
+
- You never create, split, re-scope or revise stories, and never delegate to another agent. A finding that is too big for this story is a **question** in the review file with the evidence and the options – the developer decides whether to widen the story, open a question in `OPEN_QUESTIONS.md`, or fix it now.
|
|
58
|
+
- You review the diff, not the whole codebase. Read the surrounding code only where the diff needs it to be judged.
|
|
59
|
+
- Do not restate what lint, types or tests already enforce. Report what a machine cannot see.
|
|
60
|
+
|
|
56
61
|
Reply to the main session with the verdict, the counts per severity and the file path.
|
|
@@ -1,64 +1,69 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ux-designer
|
|
3
|
-
description: UX
|
|
3
|
+
description: On-demand UX utility – component and layout proposals, Figma/design-token logic, UX guidelines and throwaway prototypes. Invoked only when a story carries `ui: true` or while the prototyping phase runs; it never gates a story and never writes stories, tests or production code.
|
|
4
4
|
tools: Read, Glob, Grep, Write, Edit
|
|
5
5
|
model: sonnet
|
|
6
6
|
color: pink
|
|
7
7
|
skills:
|
|
8
8
|
- ux-design
|
|
9
|
-
-
|
|
9
|
+
- prototype
|
|
10
10
|
hooks:
|
|
11
11
|
PreToolUse:
|
|
12
12
|
- matcher: "Write|Edit"
|
|
13
13
|
hooks:
|
|
14
14
|
- type: command
|
|
15
|
-
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/path-guard.ts" docs/product/ux-guidelines.md docs/
|
|
15
|
+
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/path-guard.ts" docs/product/ux-guidelines.md docs/prototypes/'
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
You are the UX
|
|
19
|
-
`
|
|
20
|
-
|
|
18
|
+
You are the UX utility of this project. You are **not a gatekeeper**: a story is never blocked on your review, and
|
|
19
|
+
`/implement` never waits for you. The main session calls you when a story has `ui: true`, when the prototyping phase
|
|
20
|
+
runs, or when the user asks a concrete interface question. You have NO context from the main conversation – everything
|
|
21
|
+
relevant is in `docs/` and in the code. Write in the artifact language (`languages.artifacts` in
|
|
22
|
+
`.claude/project.json`, default English); the UI texts you propose are in the project UI language (`languages.ui`).
|
|
21
23
|
|
|
22
24
|
## Yardstick
|
|
23
|
-
- `docs/product/ux-guidelines.md` – the decisions already made. **Your file.** If it does not exist yet, say so
|
|
24
|
-
|
|
25
|
+
- `docs/product/ux-guidelines.md` – the decisions already made. **Your file.** If it does not exist yet, say so and
|
|
26
|
+
propose its first content instead of judging against rules nobody agreed to.
|
|
25
27
|
- `docs/product/vision.md` – user groups, devices, constraints, non-goals
|
|
26
28
|
- `CONTEXT.md` – the glossary; UI terms use the `_UI (<lang>)_` wording exactly
|
|
27
|
-
- `docs/engineering/conventions.md` – the page frame, the design size, component rules, where the
|
|
28
|
-
|
|
29
|
-
- The method
|
|
29
|
+
- `docs/engineering/conventions.md` – the page frame, the design size, component rules, where the message catalogs
|
|
30
|
+
live. What is decided there is not yours to re-decide.
|
|
31
|
+
- The method and question set: the preloaded skill `ux-design`. The prototype method: `prototype`.
|
|
30
32
|
|
|
31
|
-
##
|
|
32
|
-
1. **
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
2. **
|
|
36
|
-
|
|
37
|
-
3. **Guidelines** – after the user approves a decision, write it into `docs/product/ux-guidelines.md` as a rule
|
|
38
|
-
|
|
39
|
-
4. **
|
|
40
|
-
|
|
33
|
+
## What you deliver
|
|
34
|
+
1. **Component proposals** – for a `ui: true` story: which components the screen is built from, which existing
|
|
35
|
+
component or pattern it reuses, the states (empty, full, rejected, no permission) and the UI wording. Concrete
|
|
36
|
+
enough to build from, not a mood board.
|
|
37
|
+
2. **Figma/design logic** – when the project has a design file: the frame, variants, component properties and the
|
|
38
|
+
token/mapping notes the implementation needs. Never invent a second design system; reuse what exists.
|
|
39
|
+
3. **Guidelines** – after the user approves a decision, write it into `docs/product/ux-guidelines.md` as a rule with
|
|
40
|
+
the date and the reason. Keep it short enough that someone reads it before writing a page.
|
|
41
|
+
4. **Prototype** – when the main session delegates a question that cannot be settled on paper, build the throwaway
|
|
42
|
+
prototype under `docs/prototypes/<slug>/`: state the question and the branch (`ui` or `logic`) at the top, use the
|
|
43
|
+
domain language, no tests, no persistence, no application code. Return the verdict and the branch pointer
|
|
44
|
+
(`prototype/<slug>`).
|
|
45
|
+
5. **Screen audit (on request only)** – when asked to compare an existing screen with the guidelines, report findings
|
|
46
|
+
and proposed fixes; do not turn it into a backlog item.
|
|
41
47
|
|
|
42
48
|
## Boundaries
|
|
43
|
-
- **You never
|
|
44
|
-
|
|
45
|
-
- You never write
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
- **No gatekeeping.** You never approve, block or re-scope a story. You may be asked for advice on a `ui: true`
|
|
50
|
+
story; you answer as a proposal. The main session and the user decide.
|
|
51
|
+
- **You never write stories.** No `docs/stories/`, no `OPEN_QUESTIONS.md` rows, no tickets. If a story is missing a
|
|
52
|
+
UI decision, return it as a concrete proposal to the main session.
|
|
53
|
+
- **You never write production code, tests or the engineering conventions.** Component names and props in your
|
|
54
|
+
proposal are design language; the implementation follows `docs/engineering/conventions.md`.
|
|
55
|
+
- A prototype is throwaway: it lives under `docs/prototypes/`, is captured on a throwaway branch and is never
|
|
56
|
+
merged. It is not application code.
|
|
57
|
+
- Don't invent domain terms. A missing term belongs in `CONTEXT.md` first, through the domain-model skill – say so,
|
|
58
|
+
don't use a synonym.
|
|
52
59
|
- Never edit `docs/stories/BACKLOG.md`; it is generated.
|
|
53
60
|
|
|
54
61
|
## Output (reply to the main session)
|
|
55
|
-
A
|
|
62
|
+
A short proposal per request:
|
|
56
63
|
|
|
57
|
-
|
|
|
64
|
+
| Screen / story | Component or pattern | States covered | UI wording | Open question |
|
|
65
|
+
|---|---|---|---|---|
|
|
58
66
|
|
|
59
|
-
Below it: the
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Severity means: **blocker** – the person cannot do the job, or cannot on a phone; **major** – they can, but will
|
|
63
|
-
get it wrong, misread it or not find it; **minor** – it works and is understood, but is inconsistent or clumsy.
|
|
64
|
-
Be honest about the difference; calling polish a blocker costs you the next argument.
|
|
67
|
+
Below it: the Figma/token notes, the guidelines you would add, and at most 3 questions for the user, each with a
|
|
68
|
+
recommended answer. When you built a prototype, add its question, branch, verdict and pointer (`prototype/<slug>`).
|
|
69
|
+
No verdict on the story itself.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* PostToolUse hook: validates discovery artifacts right after every write.
|
|
3
3
|
*
|
|
4
4
|
* - docs/domain/events.yaml → schema + references; on success event-storming.md and BACKLOG.md are re-rendered
|
|
5
|
-
* - docs/stories/*.md →
|
|
5
|
+
* - docs/stories/*.md → the same deterministic rules as `skills/user-stories/scripts/lint-story.ts`
|
|
6
|
+
* (frontmatter, aggregate/command/events, Gherkin, scenario limit) and the status change
|
|
6
7
|
* since the last commit (engineering workflow); on success BACKLOG.md is re-rendered
|
|
7
8
|
* Exit 2 + stderr = Claude gets the errors back and has to fix them.
|
|
8
9
|
*/
|
package/.claude/lib/config.ts
CHANGED
|
@@ -49,6 +49,7 @@ export const DEFAULT_LABEL_PREFIX = "kataster";
|
|
|
49
49
|
export const DEFAULT_STATUS_MAP: Record<string, string> = {
|
|
50
50
|
draft: "To Do",
|
|
51
51
|
review: "In Review",
|
|
52
|
+
"ready-for-human-review": "Ready for Review",
|
|
52
53
|
ready: "Ready",
|
|
53
54
|
"in-progress": "In Progress",
|
|
54
55
|
done: "Done",
|
package/.claude/lib/discovery.ts
CHANGED
|
@@ -44,11 +44,32 @@ const KIND_LABEL: Record<string, string> = {
|
|
|
44
44
|
* `superseded`: never built as written – its work was done elsewhere (a direct change, another story) or a new planning
|
|
45
45
|
* replaces it. The file stays as the record other documents link to; it is final and counts toward no progress (user,
|
|
46
46
|
* 2026-10-25).
|
|
47
|
+
*
|
|
48
|
+
* `ready-for-human-review` is set by the linter gate: a story whose spec passes every mechanical check waits there
|
|
49
|
+
* until the user approves it as `ready`. Engineering only picks up `ready` stories.
|
|
50
|
+
*/
|
|
51
|
+
export const STATUSES = ["draft", "review", "ready-for-human-review", "ready", "in-progress", "done", "superseded"] as const;
|
|
52
|
+
/** Statuses that require a finished story: no [OPEN] markers and a consistent spec. */
|
|
53
|
+
export const COMMITTED = new Set(["ready-for-human-review", "ready", "in-progress", "done"]);
|
|
54
|
+
/** Statuses handed to engineering or the tracker: the tracker link is required from `ready` on, not before. */
|
|
55
|
+
const HANDOFF = new Set(["ready", "in-progress", "done"]);
|
|
56
|
+
/**
|
|
57
|
+
* 1 command + 1 aggregate, at most 4 scenarios: one happy path and up to three edge/error cases. Each scenario is a
|
|
58
|
+
* red → green cycle in /implement; more than four means the story merges commands or restates UX guidelines.
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_SCENARIOS = 4;
|
|
61
|
+
/**
|
|
62
|
+
* Story types.
|
|
63
|
+
*
|
|
64
|
+
* domain – full DDD/TDD path: exactly one command on one aggregate, Gherkin traceability, reviews
|
|
65
|
+
* chore/task – fast lane: UI tweaks, refactorings, dependencies, setup; no Gherkin matching, no formal reviews
|
|
66
|
+
* tech-task – deliberate technical groundwork with a checklist
|
|
67
|
+
* spike – a timeboxed question
|
|
68
|
+
* story – the legacy name of `domain`; still valid, reported as a warning
|
|
47
69
|
*/
|
|
48
|
-
export const
|
|
49
|
-
|
|
50
|
-
export const
|
|
51
|
-
export const MAX_SCENARIOS = 10;
|
|
70
|
+
export const DOMAIN_TYPES = new Set(["domain", "story"]);
|
|
71
|
+
export const FAST_TYPES = new Set(["chore", "task"]);
|
|
72
|
+
export const LEGACY_TYPES: Record<string, string> = { story: "domain" };
|
|
52
73
|
const PRIORITIES = ["must", "should", "could", "wont"];
|
|
53
74
|
|
|
54
75
|
// --------------------------------------------------------------------------
|
|
@@ -82,6 +103,13 @@ const list = (v: unknown): string[] => (Array.isArray(v) ? v : []);
|
|
|
82
103
|
const items = (model: Obj, key: string): Obj[] => list(model[key]) as unknown as Obj[];
|
|
83
104
|
const prefix = (id: string): string => id.split("-", 1)[0];
|
|
84
105
|
|
|
106
|
+
/** Domain story: full DDD/TDD path. `story` is the legacy name of `domain`. */
|
|
107
|
+
export const isDomain = (meta: Obj): boolean => DOMAIN_TYPES.has(String(meta.type));
|
|
108
|
+
/** Fast lane: chore or task – no Gherkin matching, no formal subagent reviews. */
|
|
109
|
+
export const isFastLane = (meta: Obj): boolean => FAST_TYPES.has(String(meta.type));
|
|
110
|
+
/** A story with a frontend part: the explicit `ui: true` flag, or the legacy `ui` label. */
|
|
111
|
+
export const isUi = (meta: Obj): boolean => meta.ui === true || list(meta.labels).includes("ui");
|
|
112
|
+
|
|
85
113
|
/** Resolves symlinks where possible so paths from hooks and from ROOT compare equal. */
|
|
86
114
|
export function realPath(p: string): string {
|
|
87
115
|
try {
|
|
@@ -489,8 +517,8 @@ export const SCENARIO_RE = /^\s*(?:Scenario Outline|Scenario Template|Scenario|E
|
|
|
489
517
|
|
|
490
518
|
function checkBody(r: Report, where: string, meta: Obj, body: string): void {
|
|
491
519
|
const secs = sections(body);
|
|
492
|
-
const type = meta.type;
|
|
493
|
-
if (
|
|
520
|
+
const type = String(meta.type);
|
|
521
|
+
if (isDomain(meta)) {
|
|
494
522
|
const s = section(secs, "story");
|
|
495
523
|
if (s === null) {
|
|
496
524
|
r.error(where, "section '## Story' is missing");
|
|
@@ -499,7 +527,9 @@ function checkBody(r: Report, where: string, meta: Obj, body: string): void {
|
|
|
499
527
|
}
|
|
500
528
|
}
|
|
501
529
|
if (type === "spike" && section(secs, "question") === null) r.error(where, "spike needs a '## Question' section");
|
|
502
|
-
if (type === "tech-task"
|
|
530
|
+
if (type === "tech-task" || type === "task" || type === "chore") {
|
|
531
|
+
if (section(secs, "task") === null) r.error(where, `${type} needs a '## Task' section`);
|
|
532
|
+
}
|
|
503
533
|
|
|
504
534
|
const ac = section(secs, "acceptance criteria");
|
|
505
535
|
if (ac === null) {
|
|
@@ -511,12 +541,11 @@ function checkBody(r: Report, where: string, meta: Obj, body: string): void {
|
|
|
511
541
|
const titles = scenarios.map((sc) => sc.split("\n")[0].trim().toLowerCase());
|
|
512
542
|
const dupes = titles.filter((t, n) => t && titles.indexOf(t) !== n);
|
|
513
543
|
if (dupes.length) r.error(where, `scenario titles must be unique within the story: ${[...new Set(dupes)].join(", ")}`);
|
|
514
|
-
if (
|
|
544
|
+
if (isDomain(meta)) {
|
|
515
545
|
if (scenarios.length < 2) r.error(where, "at least 2 Gherkin scenarios required (happy path + error/edge case)");
|
|
516
546
|
scenarios.forEach((sc, n) => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
547
|
+
const missing = ["Given", "When", "Then"].filter((step) => !new RegExp(`^\\s*${step}\\b`, "m").test(sc));
|
|
548
|
+
if (missing.length) r.error(where, `scenario ${n + 1}: ${missing.join(", ")} required (Gherkin Given/When/Then)`);
|
|
520
549
|
});
|
|
521
550
|
} else if (!scenarios.length && !/^\s*[-*]\s*\[[ xX]\]/m.test(ac)) {
|
|
522
551
|
r.error(where, "give acceptance criteria as scenarios or a checklist ('- [ ] …')");
|
|
@@ -554,8 +583,8 @@ export function scenarioTitlesIn(body: string): string[] {
|
|
|
554
583
|
* and stays the authority.
|
|
555
584
|
*/
|
|
556
585
|
export function completenessWarnings(story: Story): string[] {
|
|
557
|
-
if (story.meta
|
|
558
|
-
if (!
|
|
586
|
+
if (!isDomain(story.meta)) return []; // fast lane, spikes and tech tasks carry checklists, not screens
|
|
587
|
+
if (!isUi(story.meta)) return [];
|
|
559
588
|
const ac = section(sections(story.body), "acceptance criteria") ?? "";
|
|
560
589
|
if (!ac.trim()) return [];
|
|
561
590
|
const titles = scenarioTitlesIn(story.body);
|
|
@@ -638,24 +667,90 @@ export function validateStories(report: Report, only: string | null = null, leni
|
|
|
638
667
|
else r.error(where, msg);
|
|
639
668
|
}
|
|
640
669
|
}
|
|
641
|
-
const status = m.status;
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
670
|
+
const status = String(m.status);
|
|
671
|
+
const type = String(m.type);
|
|
672
|
+
// The strict 1-command rules apply to the new `domain` type. `story` is the legacy name and stays valid
|
|
673
|
+
// (warnings only), so an upgrade does not turn an existing backlog red.
|
|
674
|
+
const strict = type === "domain";
|
|
675
|
+
if (LEGACY_TYPES[type]) {
|
|
676
|
+
r.warn(where, `type '${type}' is the legacy name of '${LEGACY_TYPES[type]}' – use '${LEGACY_TYPES[type]}'`);
|
|
677
|
+
}
|
|
678
|
+
if (!isDomain(m) && !isFastLane(m) && type !== "spike" && type !== "tech-task") {
|
|
679
|
+
r.error(where, `unknown type '${type}' (domain, chore, task, tech-task, spike)`);
|
|
647
680
|
}
|
|
648
|
-
|
|
649
|
-
|
|
681
|
+
if (strict) {
|
|
682
|
+
if (!m.context) r.error(where, "domain story needs a bounded context ('context: BC-…')");
|
|
683
|
+
if (!m.aggregate) r.error(where, "domain story needs exactly one aggregate ('aggregate: AGG-…')");
|
|
684
|
+
if (!m.command) r.error(where, "domain story needs exactly one command ('command: CMD-…')");
|
|
685
|
+
}
|
|
686
|
+
if (isDomain(m) && !strict) {
|
|
687
|
+
if (!m.aggregate || !m.command) {
|
|
688
|
+
r.warn(where, "legacy story without aggregate/command – the 1-command rule cannot be checked; migrate it to type: domain");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (idx !== null) {
|
|
692
|
+
const ctx = m.context;
|
|
693
|
+
if (typeof ctx === "string" && ctx && (!idx.has(ctx) || prefix(ctx) !== "BC")) {
|
|
694
|
+
r.error(where, `context '${ctx}' does not exist in events.yaml`);
|
|
695
|
+
}
|
|
696
|
+
for (const ev of list(m.events)) {
|
|
697
|
+
if (!idx.has(ev) || prefix(ev) !== "EVT") r.error(where, `event '${ev}' does not exist in events.yaml`);
|
|
698
|
+
}
|
|
699
|
+
if (m.aggregate !== undefined) {
|
|
700
|
+
if (typeof m.aggregate !== "string" || !idx.has(m.aggregate) || prefix(m.aggregate) !== "AGG") {
|
|
701
|
+
r.error(where, `aggregate '${fmt(m.aggregate)}' does not exist in events.yaml`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (m.command !== undefined) {
|
|
705
|
+
if (typeof m.command !== "string" || !idx.has(m.command) || prefix(m.command) !== "CMD") {
|
|
706
|
+
r.error(where, `command '${fmt(m.command)}' does not exist in events.yaml`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// The 1-command rule: one story = one command + its events on one aggregate.
|
|
710
|
+
const cmd = typeof m.command === "string" ? idx.get(m.command) : undefined;
|
|
711
|
+
if (cmd && typeof m.aggregate === "string" && cmd.aggregate && cmd.aggregate !== m.aggregate) {
|
|
712
|
+
r.error(where, `command '${m.command}' belongs to aggregate '${cmd.aggregate}', not '${m.aggregate}'`);
|
|
713
|
+
}
|
|
714
|
+
if (strict && cmd) {
|
|
715
|
+
const produced = list(cmd.produces);
|
|
716
|
+
const linked = list(m.events);
|
|
717
|
+
if (!produced.length) {
|
|
718
|
+
r.error(where, `command '${m.command}' produces no event in events.yaml`);
|
|
719
|
+
} else {
|
|
720
|
+
const missing = produced.filter((ev) => !linked.includes(ev));
|
|
721
|
+
const extra = linked.filter((ev) => !produced.includes(ev));
|
|
722
|
+
if (missing.length || extra.length) {
|
|
723
|
+
r.error(
|
|
724
|
+
where,
|
|
725
|
+
`events must be exactly what command '${m.command}' produces (${produced.join(", ")})` +
|
|
726
|
+
(missing.length ? ` – missing: ${missing.join(", ")}` : "") +
|
|
727
|
+
(extra.length ? ` – not produced: ${extra.join(", ")}` : ""),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} else if (isDomain(m)) {
|
|
733
|
+
// Without a readable model the required fields are checked above; the cross-check cannot run.
|
|
734
|
+
r.warn(where, "events.yaml is missing or invalid – the aggregate/command cross-check was skipped");
|
|
735
|
+
}
|
|
736
|
+
for (const dep of list(m.depends_on)) {
|
|
737
|
+
if (dep === id) {
|
|
738
|
+
r.error(where, "depends on itself");
|
|
739
|
+
} else if (!stories.has(dep)) {
|
|
740
|
+
const msg = `depends_on '${dep}' does not exist`;
|
|
741
|
+
if (lenientDeps) r.warn(where, msg);
|
|
742
|
+
else r.error(where, msg);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (m.size === "XL") r.warn(where, "size XL – split the story");
|
|
746
|
+
// Every scenario is a red → green cycle in /implement; past four the story merges commands or restates UX
|
|
747
|
+
// guideline patterns. Domain stories must obey it; legacy `story` files get a warning until migrated.
|
|
650
748
|
const scenarioTotal = scenarioTitlesIn(s.body).length;
|
|
651
|
-
if (m
|
|
652
|
-
const msg = `${scenarioTotal} scenarios, at most ${MAX_SCENARIOS} – split the story, or reference the UX guideline (G-number) instead of restating its pattern as scenarios`;
|
|
653
|
-
if (status
|
|
749
|
+
if (isDomain(m) && scenarioTotal > MAX_SCENARIOS && (status === "draft" || status === "review" || status === "ready-for-human-review")) {
|
|
750
|
+
const msg = `${scenarioTotal} scenarios, at most ${MAX_SCENARIOS} – split the story (one command, one aggregate), or reference the UX guideline (G-number) instead of restating its pattern as scenarios`;
|
|
751
|
+
if (strict && status !== "draft") r.error(where, msg);
|
|
654
752
|
else r.warn(where, msg);
|
|
655
753
|
}
|
|
656
|
-
if (m.type === "story" && COMMITTED.has(status) && !list(m.events).length) {
|
|
657
|
-
r.warn(where, "no domain events linked – traceability is missing");
|
|
658
|
-
}
|
|
659
754
|
|
|
660
755
|
// In tracker mode the Markdown file is a mirror. A story that is handed to engineering must carry its
|
|
661
756
|
// external key, otherwise the next pull cannot match it and the tracker is not the source of truth any more.
|
|
@@ -668,7 +763,7 @@ export function validateStories(report: Report, only: string | null = null, leni
|
|
|
668
763
|
r.error(where, "external.key is missing");
|
|
669
764
|
}
|
|
670
765
|
}
|
|
671
|
-
if (
|
|
766
|
+
if (HANDOFF.has(status) && !(ext && typeof ext === "object" && typeof ext.key === "string" && ext.key)) {
|
|
672
767
|
r.error(where, `not linked to the ${backlog.provider} backlog – run /backlog-sync push before '${status}'`);
|
|
673
768
|
}
|
|
674
769
|
const prefix = labelPrefix(backlog);
|
|
@@ -680,7 +775,9 @@ export function validateStories(report: Report, only: string | null = null, leni
|
|
|
680
775
|
checkBody(r, where, m, s.body);
|
|
681
776
|
// Completeness is checked while a story is being written; `ready` ones were approved under the older rules
|
|
682
777
|
// and are challenged in `/groom-backlog` instead (user, 2026-10-01).
|
|
683
|
-
if (status === "draft" || status === "review"
|
|
778
|
+
if (status === "draft" || status === "review" || status === "ready-for-human-review") {
|
|
779
|
+
for (const w of completenessWarnings(s)) r.warn(where, w);
|
|
780
|
+
}
|
|
684
781
|
}
|
|
685
782
|
|
|
686
783
|
// Cycles in depends_on
|
|
@@ -710,6 +807,7 @@ export function validateStories(report: Report, only: string | null = null, leni
|
|
|
710
807
|
const STATUS_TITLES: [string, string][] = [
|
|
711
808
|
["in-progress", "In Progress"],
|
|
712
809
|
["ready", "Ready"],
|
|
810
|
+
["ready-for-human-review", "Ready for Human Review"],
|
|
713
811
|
["review", "In Review"],
|
|
714
812
|
["draft", "Draft"],
|
|
715
813
|
["done", "Done"],
|
|
@@ -763,14 +861,14 @@ export function renderBacklog(stories: Story[], model: Obj | null): string {
|
|
|
763
861
|
);
|
|
764
862
|
}
|
|
765
863
|
if (!stories.length) {
|
|
766
|
-
out.push("_No stories yet. Derive them from the event storming
|
|
864
|
+
out.push("_No stories yet. Derive them from the event storming as a spec engineer (main session)._", "");
|
|
767
865
|
return out.join("\n");
|
|
768
866
|
}
|
|
769
867
|
|
|
770
868
|
const counts = STATUS_TITLES.map(([st, title]) => [title, stories.filter((s) => s.meta.status === st).length] as const);
|
|
771
869
|
// Progress is measured in domain stories and their scenarios, not in the total number of stories (CLAUDE.md).
|
|
772
870
|
// A superseded story was never built as one – its work counts where it was done.
|
|
773
|
-
const domain = stories.filter((s) => s.meta
|
|
871
|
+
const domain = stories.filter((s) => isDomain(s.meta) && s.meta.status !== "superseded");
|
|
774
872
|
const scenarioCount = (s: Story) =>
|
|
775
873
|
(section(sections(s.body), "acceptance criteria") ?? "")
|
|
776
874
|
.split("\n")
|
|
@@ -66,13 +66,15 @@ export const testTitle = (id: string, scenario: string): string => `${id}: ${sce
|
|
|
66
66
|
// Status workflow
|
|
67
67
|
// --------------------------------------------------------------------------
|
|
68
68
|
/**
|
|
69
|
-
* Allowed single steps. Discovery owns draft/review/ready;
|
|
70
|
-
* and sends a story back to review when it turns
|
|
71
|
-
* behaviour become a new story.
|
|
69
|
+
* Allowed single steps. Discovery owns draft/review/ready-for-human-review; the linter sets the review state, the
|
|
70
|
+
* user approves `ready`. Engineering moves ready → in-progress → done and sends a story back to review when it turns
|
|
71
|
+
* out to be wrong. `done` is final: changes to finished behaviour become a new story.
|
|
72
72
|
*/
|
|
73
73
|
const TRANSITIONS: Record<string, string[]> = {
|
|
74
74
|
draft: ["review", "superseded"],
|
|
75
|
-
review: ["draft", "ready", "superseded"],
|
|
75
|
+
review: ["draft", "ready-for-human-review", "superseded"],
|
|
76
|
+
// The linter gate passed; only the user turns this into `ready`.
|
|
77
|
+
"ready-for-human-review": ["draft", "review", "ready", "superseded"],
|
|
76
78
|
ready: ["review", "in-progress", "superseded"],
|
|
77
79
|
"in-progress": ["review", "done"],
|
|
78
80
|
done: [],
|
|
@@ -106,7 +108,7 @@ export function transitionErrors(story: d.Story, from: string, to: string, stori
|
|
|
106
108
|
errs.push(
|
|
107
109
|
from === "done" || from === "superseded"
|
|
108
110
|
? `a ${from} story is never reopened – capture the change as a new story`
|
|
109
|
-
: `status '${from}' cannot become '${to}' (workflow: draft → review → ready → in-progress → done, back to review when a story is wrong; superseded from any status before in-progress)`,
|
|
111
|
+
: `status '${from}' cannot become '${to}' (workflow: draft → review → ready-for-human-review → ready → in-progress → done, back to review when a story is wrong; superseded from any status before in-progress)`,
|
|
110
112
|
);
|
|
111
113
|
return errs;
|
|
112
114
|
}
|
|
@@ -114,8 +116,12 @@ export function transitionErrors(story: d.Story, from: string, to: string, stori
|
|
|
114
116
|
const open = list(story.meta.depends_on).filter((dep) => stories.get(dep)?.meta.status !== "done");
|
|
115
117
|
if (open.length) errs.push(`depends on stories that are not done yet: ${open.join(", ")}`);
|
|
116
118
|
}
|
|
119
|
+
// The linter gate and the user's approval are two separate steps; skipping one defeats the workflow.
|
|
120
|
+
if (to === "ready" && from !== "ready-for-human-review") {
|
|
121
|
+
errs.push("'ready' needs 'ready-for-human-review' first – run the spec linter and let the user approve");
|
|
122
|
+
}
|
|
117
123
|
if (to === "done") {
|
|
118
|
-
if (story.meta
|
|
124
|
+
if (d.isDomain(story.meta)) {
|
|
119
125
|
const cov = scenarioCoverage(story);
|
|
120
126
|
if (cov.missing.length) {
|
|
121
127
|
errs.push(`scenarios without a test titled '${d.storyId(story)}: <scenario title>': ${cov.missing.map((m) => `"${m}"`).join(", ")}`);
|
package/.claude/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"node": ">=22.18"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"lint:story": "node skills/user-stories/scripts/lint-story.ts",
|
|
10
11
|
"validate": "node skills/event-storming/scripts/validate-events.ts && node skills/user-stories/scripts/validate-stories.ts",
|
|
11
12
|
"render": "node skills/event-storming/scripts/render-event-storming.ts && node skills/user-stories/scripts/render-backlog.ts",
|
|
12
13
|
"typecheck": "tsc -p ."
|
package/.claude/settings.json
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
"Bash(node .claude/skills/adopt/scripts/*)",
|
|
6
6
|
"Bash(node .claude/skills/event-storming/scripts/validate-events.ts)",
|
|
7
7
|
"Bash(node .claude/skills/event-storming/scripts/render-event-storming.ts)",
|
|
8
|
-
"Bash(node .claude/skills/user-stories/scripts
|
|
9
|
-
"Bash(node .claude/skills/user-stories/scripts/render-backlog.ts)",
|
|
8
|
+
"Bash(node .claude/skills/user-stories/scripts/*)",
|
|
10
9
|
"Bash(node .claude/skills/implement/scripts/*)",
|
|
11
10
|
"Bash(git status *)",
|
|
12
11
|
"Bash(git diff *)",
|
|
@@ -211,9 +211,11 @@ user has not prioritised.
|
|
|
211
211
|
|
|
212
212
|
Only when the invocation contains `--stories`, or the user explicitly asks after seeing the table:
|
|
213
213
|
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
- write the stories yourself as the spec engineer (the `user-stories` skill is the method) with the **selected**
|
|
215
|
+
gaps only, the confirmed IDs from `events.yaml`, `type: domain` with one `command` and its `aggregate`, `ui: true`
|
|
216
|
+
for stories with a frontend part, and the label `adoption:<slug>`;
|
|
217
|
+
- new stories start as `status: draft`; the ones the user wants first go through the spec linter and
|
|
218
|
+
`/review-stories`.
|
|
217
219
|
|
|
218
220
|
**Do not write stories for behaviour that already works** – that turns the backlog into a description of the
|
|
219
221
|
past. Existing behaviour is covered by its tests, and the domain model is the map. If an earlier run created
|
|
@@ -155,8 +155,8 @@ silently – ask the user.
|
|
|
155
155
|
|
|
156
156
|
## When to sync
|
|
157
157
|
|
|
158
|
-
- after
|
|
159
|
-
- after `
|
|
158
|
+
- after the main session created or changed stories (spec engineer) → `push`
|
|
159
|
+
- after `priority`, `size`, `risk` or `depends_on` changed → `push`
|
|
160
160
|
- before `/review-stories` → `pull`, then review, then `push` the approved stories
|
|
161
161
|
- before `/implement` → `pull`; after each status change the workflow updates the tracker (GitHub directly, Jira by
|
|
162
162
|
the printed MCP transition), and after `done` → `push`
|