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.
Files changed (59) hide show
  1. package/.claude/THIRD_PARTY_NOTICES.md +69 -0
  2. package/.claude/agents/acceptance-tester.md +7 -2
  3. package/.claude/agents/code-reviewer.md +7 -2
  4. package/.claude/agents/ux-designer.md +43 -38
  5. package/.claude/hooks/validate-docs.ts +2 -1
  6. package/.claude/lib/config.ts +1 -0
  7. package/.claude/lib/discovery.ts +129 -31
  8. package/.claude/lib/engineering.ts +12 -6
  9. package/.claude/package.json +1 -0
  10. package/.claude/settings.json +1 -2
  11. package/.claude/skills/adopt/SKILL.md +5 -3
  12. package/.claude/skills/backlog/SKILL.md +2 -2
  13. package/.claude/skills/bootstrap/SKILL.md +9 -7
  14. package/.claude/skills/domain-model/SKILL.md +1 -1
  15. package/.claude/skills/feature/SKILL.md +37 -11
  16. package/.claude/skills/feature/scripts/intake.ts +1 -1
  17. package/.claude/skills/groom-backlog/SKILL.md +19 -16
  18. package/.claude/skills/implement/SKILL.md +169 -63
  19. package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
  20. package/.claude/skills/implement/scripts/story-context.ts +1 -1
  21. package/.claude/skills/implement/scripts/verify.ts +1 -1
  22. package/.claude/skills/prototype/LOGIC.md +101 -0
  23. package/.claude/skills/prototype/SKILL.md +85 -0
  24. package/.claude/skills/prototype/UI.md +130 -0
  25. package/.claude/skills/review-stories/SKILL.md +39 -19
  26. package/.claude/skills/shape/SKILL.md +2 -2
  27. package/.claude/skills/user-stories/SKILL.md +109 -51
  28. package/.claude/skills/user-stories/schema/story.schema.json +6 -3
  29. package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
  30. package/.claude/skills/user-stories/templates/story-template.md +5 -2
  31. package/.claude/skills/ux-design/SKILL.md +30 -5
  32. package/.opencode/agents/acceptance-tester.md +8 -3
  33. package/.opencode/agents/code-reviewer.md +8 -3
  34. package/.opencode/agents/ux-designer.md +44 -39
  35. package/.opencode/commands/bootstrap.md +1 -1
  36. package/.opencode/commands/feature.md +1 -1
  37. package/.opencode/commands/groom-backlog.md +1 -1
  38. package/.opencode/commands/implement.md +1 -1
  39. package/.opencode/commands/review-stories.md +1 -1
  40. package/AGENTS.md +17 -0
  41. package/CHANGELOG.md +38 -0
  42. package/README.md +64 -7
  43. package/guide/brownfield.md +2 -2
  44. package/guide/concepts.md +26 -5
  45. package/guide/greenfield.md +4 -3
  46. package/guide/release.md +1 -1
  47. package/package.json +1 -1
  48. package/src/cli.mjs +42 -4
  49. package/src/install.mjs +62 -6
  50. package/templates/docs/product/ux-guidelines.md +2 -0
  51. package/templates/docs/prototypes/README.md +9 -0
  52. package/templates/docs/stories/README.md +10 -3
  53. package/templates/docs/workflow.md +38 -13
  54. package/.claude/agents/lead-dev.md +0 -50
  55. package/.claude/agents/product-owner.md +0 -39
  56. package/.claude/agents/requirements-engineer.md +0 -44
  57. package/.opencode/agents/lead-dev.md +0 -67
  58. package/.opencode/agents/product-owner.md +0 -59
  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 / new story / question |
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), **follow-up** (worth a tech-task story, too big for this one).
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/UI designer. Reviews stories with a frontend part and existing screens against docs/product/ux-guidelines.md, and maintains those guidelines. Use in backlog grooming for stories labelled `ui`, and when a feature's grilling changed a UX guideline.
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
- - user-stories
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/reviews/'
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/UI designer of this project, whose interface its users meet in the situations
19
- `docs/product/vision.md` describes. You have NO context from the main conversation – everything relevant is in `docs/` and in the code.
20
- Write in the artifact language (`languages.artifacts` in `.claude/project.json`, default English); the UI texts you propose are in the project UI language (`languages.ui` in `.claude/project.json`).
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
- and propose its first content instead of judging against rules nobody agreed to.
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
- message catalogs live. What is decided there is not yours to re-decide.
29
- - The method, the question set and the review order: the preloaded skill `ux-design`.
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
- ## Tasks
32
- 1. **Story review** – per story with a frontend part: is the screen's job clear, is the entry point named, do the
33
- empty, rejected and no-permission cases have a scenario, is the UI wording decided and does it match the
34
- glossary? Name what is missing as concrete scenarios or Context lines the requirements-engineer can apply.
35
- 2. **Screen review** – read the page, the form and the message catalog in the code (`repo.codeRoots` in `.claude/project.json`), and judge in the order the
36
- skill gives. Findings only; you never change application code.
37
- 3. **Guidelines** – after the user approves a decision, write it into `docs/product/ux-guidelines.md` as a rule,
38
- with the date and the reason. Keep it short enough that someone reads it before writing a page.
39
- 4. **Gaps** – which recurring interface question has no guideline yet, and which existing screens contradict each
40
- other?
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 write stories.** Everything that belongs in a story comes back as a recommendation – the
44
- `requirements-engineer` applies it. The same rule the product-owner and the lead-dev follow.
45
- - You never write application code, tests or the engineering conventions.
46
- - No implementation detail in what you propose: no component names, no classes, no pixel values. Observable
47
- behaviour and words, not markup.
48
- - Make no assumptions where the vision or the guidelines are silent: return them as questions, each with a
49
- recommended answer, as the `grilling` skill requires.
50
- - Don't invent domain terms. A term that is missing belongs in `CONTEXT.md` first, through the domain-model skill
51
- – say so, don't use a synonym.
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 table of findings:
62
+ A short proposal per request:
56
63
 
57
- | Story / screen | Level (job / understanding / states / language / consistency / polish) | Severity (blocker, major, minor) | Finding | What to do instead |
64
+ | Screen / story | Component or pattern | States covered | UI wording | Open question |
65
+ |---|---|---|---|---|
58
66
 
59
- Below it: the scenarios or Context lines you propose per story (ready for the requirements-engineer to apply), the
60
- guidelines you would add, and at most 3 questions for the user, each with a recommended answer.
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 → validation of the changed story (lenient on dependencies) and of its status change
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
  */
@@ -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",
@@ -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 STATUSES = ["draft", "review", "ready", "in-progress", "done", "superseded"] as const;
49
- /** Statuses that require a finished story: size set, no [OPEN] markers. */
50
- export const COMMITTED = new Set(["ready", "in-progress", "done"]);
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 (type === "story") {
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" && section(secs, "task") === null) r.error(where, "tech task needs a '## Task' section");
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 (type === "story") {
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
- if (!/^\s*When\b/m.test(sc) || !/^\s*Then\b/m.test(sc)) {
518
- r.error(where, `scenario ${n + 1}: 'When' and 'Then' are required`);
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.type !== "story") return []; // spikes and tech tasks carry checklists, not screens
558
- if (!list(story.meta.labels).includes("ui")) return [];
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
- if (COMMITTED.has(status) && !m.size) r.error(where, `size is missing – lead-dev must estimate before '${status}'`);
643
- if (m.size === "XL") {
644
- const msg = "size XL – split the story";
645
- if (COMMITTED.has(status)) r.error(where, msg);
646
- else r.warn(where, msg);
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
- // Every scenario is a red → green cycle in /implement; past ten the story is too big or restates UX
649
- // guideline patterns (user, 2026-10-26). `ready` ones were approved under the older rules.
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.type === "story" && scenarioTotal > MAX_SCENARIOS && (status === "draft" || status === "review")) {
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 === "review") r.error(where, msg);
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 (COMMITTED.has(status) && !(ext && typeof ext === "object" && typeof ext.key === "string" && ext.key)) {
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") for (const w of completenessWarnings(s)) r.warn(where, w);
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 with `@requirements-engineer`._", "");
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.type === "story" && s.meta.status !== "superseded");
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; engineering moves ready → in-progress → done
70
- * and sends a story back to review when it turns out to be wrong. `done` is final: changes to finished
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.type === "story") {
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(", ")}`);
@@ -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 ."
@@ -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/validate-stories.ts)",
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
- - delegate to `requirements-engineer` with the **selected** gaps only, the confirmed IDs from `events.yaml`, the
215
- label `adoption:<slug>`, and `ui` for stories with a frontend part;
216
- - new stories start as `status: draft`; the ones the user wants first go through `/review-stories`.
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 `requirements-engineer` created or changed stories → `push`
159
- - after `product-owner` or `lead-dev` changed `priority`, `size`, `risk`, `depends_on` → `push`
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`