kataster 0.3.1 → 0.6.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 (89) hide show
  1. package/.claude/THIRD_PARTY_NOTICES.md +4 -0
  2. package/.claude/agents/acceptance-tester.md +7 -2
  3. package/.claude/agents/code-reviewer.md +7 -2
  4. package/.claude/agents/domain-architect.md +7 -3
  5. package/.claude/agents/researcher.md +77 -0
  6. package/.claude/agents/ux-designer.md +42 -47
  7. package/.claude/hooks/validate-docs.ts +17 -1
  8. package/.claude/lib/architecture.ts +3 -33
  9. package/.claude/lib/backlog.ts +2 -0
  10. package/.claude/lib/config.ts +49 -0
  11. package/.claude/lib/discovery.ts +200 -32
  12. package/.claude/lib/engineering.ts +18 -6
  13. package/.claude/lib/gaps.ts +179 -0
  14. package/.claude/lib/glob.ts +39 -0
  15. package/.claude/lib/repo.ts +143 -19
  16. package/.claude/lib/research.ts +303 -0
  17. package/.claude/lib/slices.ts +529 -0
  18. package/.claude/package.json +1 -0
  19. package/.claude/settings.json +2 -2
  20. package/.claude/skills/adopt/SKILL.md +111 -34
  21. package/.claude/skills/adopt/scripts/gap-register.ts +61 -0
  22. package/.claude/skills/adopt/scripts/repo-map.ts +34 -2
  23. package/.claude/skills/adopt/scripts/slice-doc.ts +117 -0
  24. package/.claude/skills/adopt/scripts/slice.ts +163 -0
  25. package/.claude/skills/adopt/templates/context-map.as-is.md +22 -0
  26. package/.claude/skills/architect/SKILL.md +6 -1
  27. package/.claude/skills/backlog/SKILL.md +2 -2
  28. package/.claude/skills/bootstrap/SKILL.md +9 -7
  29. package/.claude/skills/domain-model/CONTEXT-FORMAT.md +1 -1
  30. package/.claude/skills/domain-model/SKILL.md +24 -4
  31. package/.claude/skills/event-storming/SKILL.md +3 -2
  32. package/.claude/skills/event-storming/schema/events.schema.json +2 -0
  33. package/.claude/skills/event-storming/templates/events.example.yaml +6 -0
  34. package/.claude/skills/feature/SKILL.md +16 -13
  35. package/.claude/skills/feature/scripts/intake.ts +1 -1
  36. package/.claude/skills/grilling/SKILL.md +15 -0
  37. package/.claude/skills/groom-backlog/SKILL.md +19 -17
  38. package/.claude/skills/implement/SKILL.md +199 -78
  39. package/.claude/skills/implement/scripts/characterize.ts +252 -0
  40. package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
  41. package/.claude/skills/implement/scripts/story-context.ts +65 -1
  42. package/.claude/skills/implement/scripts/verify.ts +4 -1
  43. package/.claude/skills/improve-codebase-architecture/SKILL.md +4 -1
  44. package/.claude/skills/prototype/SKILL.md +1 -1
  45. package/.claude/skills/research/SKILL.md +149 -0
  46. package/.claude/skills/research/scripts/research-check.ts +105 -0
  47. package/.claude/skills/research/templates/research.md +29 -0
  48. package/.claude/skills/review-stories/SKILL.md +49 -19
  49. package/.claude/skills/shape/SKILL.md +26 -7
  50. package/.claude/skills/tdd/SKILL.md +10 -0
  51. package/.claude/skills/user-stories/SKILL.md +128 -51
  52. package/.claude/skills/user-stories/schema/story.schema.json +10 -3
  53. package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
  54. package/.claude/skills/user-stories/templates/story-template.md +6 -2
  55. package/.claude/skills/ux-design/SKILL.md +9 -7
  56. package/.opencode/agents/acceptance-tester.md +8 -3
  57. package/.opencode/agents/code-reviewer.md +8 -3
  58. package/.opencode/agents/domain-architect.md +7 -3
  59. package/.opencode/agents/researcher.md +91 -0
  60. package/.opencode/agents/ux-designer.md +42 -52
  61. package/.opencode/commands/adopt.md +1 -1
  62. package/.opencode/commands/bootstrap.md +1 -1
  63. package/.opencode/commands/feature.md +1 -1
  64. package/.opencode/commands/groom-backlog.md +1 -1
  65. package/.opencode/commands/implement.md +1 -1
  66. package/.opencode/commands/research.md +7 -0
  67. package/.opencode/commands/review-stories.md +1 -1
  68. package/.opencode/commands/shape.md +1 -1
  69. package/AGENTS.md +30 -0
  70. package/CHANGELOG.md +73 -0
  71. package/README.md +108 -24
  72. package/guide/brownfield.md +41 -12
  73. package/guide/concepts.md +23 -6
  74. package/guide/greenfield.md +4 -3
  75. package/package.json +1 -1
  76. package/src/cli.mjs +42 -4
  77. package/src/detect.mjs +9 -0
  78. package/src/install.mjs +66 -6
  79. package/templates/docs/domain/context-map.md +5 -1
  80. package/templates/docs/domain/events.yaml +3 -0
  81. package/templates/docs/research/README.md +70 -0
  82. package/templates/docs/stories/README.md +12 -3
  83. package/templates/docs/workflow.md +76 -19
  84. package/.claude/agents/lead-dev.md +0 -52
  85. package/.claude/agents/product-owner.md +0 -39
  86. package/.claude/agents/requirements-engineer.md +0 -44
  87. package/.opencode/agents/lead-dev.md +0 -69
  88. package/.opencode/agents/product-owner.md +0 -59
  89. package/.opencode/agents/requirements-engineer.md +0 -58
@@ -1,138 +1,259 @@
1
1
  ---
2
2
  name: implement
3
- description: Engineering workflow – implements one story from docs/stories/ test-first on its own branch, verifies, has it reviewed by code-reviewer and acceptance-tester, refactors, and sets it `done` on the acceptance tester's verdict and hands the pull request to the user to merge; `--prototype` settles an open UI question first.
3
+ description: Engineering workflow – implements one story from docs/stories/ on its own branch, verifies, reviews scaled to the diff, refactors and hands the pull request to the user; `type: chore`/`task` (or `--fast`) takes the fast lane without Gherkin matching or formal reviews; `--prototype` settles an open UI question first.
4
4
  disable-model-invocation: true
5
- argument-hint: "[ST-NNN – empty = next ready story] [--prototype[=ui|logic]]"
5
+ argument-hint: "[ST-NNN – empty = next ready story] [--fast] [--prototype[=ui|logic]]"
6
6
  ---
7
7
 
8
8
  # Implement a Story
9
9
 
10
- Story: the arguments from the invocation (empty → `node .claude/skills/implement/scripts/next-story.ts`; a story already `in-progress` is finished first). `--prototype[=ui|logic]` is a flag, not the story ID: strip it before resolving the story. It asks for a prototype before the test plan; see *Prototype (only when asked)*.
10
+ Story: the arguments from the invocation (empty → `node .claude/skills/implement/scripts/next-story.ts`; a story
11
+ already `in-progress` is finished first). `--fast` and `--prototype[=ui|logic]` are flags, not the story ID: strip
12
+ them before resolving the story.
11
13
 
12
- The main session writes the code; subagents only review. One checkpoint belongs to the user: the **merge** (step 7). Everything before it runs without asking – a `ready` story is good enough to build up to the pull request, and the acceptance tester accepts the story, not the user – unless the story turns out to be wrong (see *When the story is wrong*), a seam is missing from the catalog (step 2), or the user asked for a prototype (step 1b – the verdict is theirs).
14
+ The main session writes the code; subagents only review, and only in the full domain lane. One checkpoint belongs to
15
+ the user: the **merge** (step 7). Everything before it runs without asking – a `ready` story is good enough to build
16
+ up to the pull request, and the acceptance tester accepts the story, not the user – unless the story turns out to be
17
+ wrong (see *When the story is wrong*), a seam is missing from the catalog (step 2), or the user asked for a prototype
18
+ (step 1b – the verdict is theirs).
13
19
 
14
- Engineering conventions: `docs/engineering/conventions.md` – read it if it exists; it describes the module layout, how commands and read models are built, and the **seam catalog**.
20
+ ## Two lanes
15
21
 
16
- Project profile: `.claude/project.json` – default branch, code roots, commands, preview provider, pull-request tool. Read it instead of assuming `main`, `npm` or GitHub.
22
+ | Lane | Stories | What changes |
23
+ |---|---|---|
24
+ | **Domain** (default) | `type: domain` (legacy `story`) | TDD per Gherkin scenario, scenario ↔ test traceability, diff-scaled review, acceptance tester's verdict |
25
+ | **Fast** | `type: chore`, `type: task`, or `--fast` on such a story | branch → implement → lint/typecheck/unit tests → verify → PR; no Gherkin matching, no formal review pass, the user merges |
26
+
27
+ `--fast` on a `type: domain` story is ignored: the story has a command and scenarios, so it takes the domain lane.
28
+ If a "chore" turns out to touch a command, a migration or authorization, stop and say so – risk decides the lane,
29
+ not the label.
30
+
31
+ Engineering conventions: `docs/engineering/conventions.md` – read it if it exists; it describes the module layout,
32
+ how commands and read models are built, and the **seam catalog**.
33
+
34
+ Project profile: `.claude/project.json` – default branch, code roots, commands, preview provider, pull-request tool.
35
+ Read it instead of assuming `main`, `npm` or GitHub.
17
36
 
18
37
  ## 1. Prepare
19
- 1. Working tree clean, on the default branch (`repo.defaultBranch`), up to date (`git pull`). Otherwise stop and tell the user.
38
+ 1. Working tree clean, on the default branch (`repo.defaultBranch`), up to date (`git pull`). Otherwise stop and tell
39
+ the user.
20
40
  2. With a tracker (`backlog.provider` ≠ `local`), pull first (`/backlog-sync pull`) and check that the story has its
21
41
  `external` key – an unlinked story never reaches `ready`, so stop and report instead of implementing a stale mirror.
22
- 3. `node .claude/skills/implement/scripts/story-context.ts ST-NNN` – the context pack: story, exact test titles, dependencies, events/commands/rules/invariants, glossary, data model, ADRs. Read it completely; it is your source, not memory.
23
- 4. `git switch -c st-NNN-<slug>` (slug from the file name), then `node .claude/skills/implement/scripts/story-status.ts ST-NNN in-progress` and commit `ST-NNN: start`.
42
+ 3. `node .claude/skills/implement/scripts/story-context.ts ST-NNN` – the context pack: story, exact test titles,
43
+ dependencies, events/commands/rules/invariants, glossary, data model, ADRs, research sources. Read it completely;
44
+ it is your source, not memory. When a scenario depends on a cited finding, read the research file listed under
45
+ `## Research sources` – findings are evidence, not a specification. If an aggregate in the pack has
46
+ `status: unresolved`, stop: the consistency boundary is not settled, so the story goes back to `review` instead
47
+ of being decided in code.
48
+ 4. `git switch -c st-NNN-<slug>` (slug from the file name), then
49
+ `node .claude/skills/implement/scripts/story-status.ts ST-NNN in-progress` and commit `ST-NNN: start`.
50
+ 5. **Pick the lane** and say it in one line: fast lane for `chore`/`task`, domain lane otherwise. The fast lane is
51
+ a shortcut for non-domain work, never a way to skip a risk review.
24
52
 
25
53
  ## 1b. Prototype (only when asked)
26
54
 
27
- Only with `--prototype`, or when the user explicitly asks during the story. A `ui` story whose screen shape is
28
- still open was not `ready`; this is the exception that settles it before the test plan. Load the `prototype`
29
- skill and follow it.
55
+ Only with `--prototype`, or when the user explicitly asks during the story. A `ui: true` story whose screen shape is
56
+ still open was not `ready`; this is the exception that settles it before the test plan. Load the `prototype` skill and
57
+ follow it.
30
58
 
31
- 1. **Standalone first.** Delegate to `ux-designer`: question, branch (`ui` or `logic`), variants/scenarios,
32
- timebox, target `docs/prototypes/<slug>/`. It writes the self-contained HTML file. The user decides.
59
+ 1. **Standalone first.** Delegate to `ux-designer`: question, branch (`ui` or `logic`), variants/scenarios, timebox,
60
+ target `docs/prototypes/<slug>/`. It writes the self-contained HTML file. The user decides.
33
61
  2. **In-app only when the question needs the real page** – an existing route, real data, the real header and
34
62
  sidebar. You build the throwaway route or the `?variant=` gate yourself, following the project's routing
35
- convention; the `ux-designer` defines the question and variants and reviews the result. The prototype stays
36
- out of the production path and is captured on `prototype/<slug>`.
37
- 3. **Capture.** Commit `docs/prototypes/<slug>/` (or the in-app prototype) on `prototype/<slug>`, return to the
38
- story branch, and pass the verdict to `requirements-engineer`: `Prototype: prototype/<slug> – <question> –
39
- <verdict>` in `## Context`; a recurring rule goes to `ux-designer` for `docs/product/ux-guidelines.md`.
40
- 4. **Then decide.** If the prototype only settles layout and the scenarios still hold, continue with step 2.
41
- If it changes behaviour or a scenario, the story goes back to `review` (see *When the story is wrong*),
42
- `requirements-engineer` revises it and `/review-stories` approves it again. The branch stays.
63
+ convention; the `ux-designer` defines the question and variants and reviews the result. The prototype stays out of
64
+ the production path and is captured on `prototype/<slug>`.
65
+ 3. **Capture.** Commit `docs/prototypes/<slug>/` (or the in-app prototype) on `prototype/<slug>`, return to the story
66
+ branch, and write the verdict into the story's `## Context`: `Prototype: prototype/<slug> – <question> – <verdict>`.
67
+ A recurring rule goes to `ux-designer` for `docs/product/ux-guidelines.md`.
68
+ 4. **Then decide.** If the prototype only settles layout and the scenarios still hold, continue with step 2. If it
69
+ changes behaviour or a scenario, the story goes back to `review` (see *When the story is wrong*), the main session
70
+ revises it, the spec linter runs and the user approves it again. The branch stays.
43
71
 
44
72
  ## 2. Test plan
45
- Call the `skill` tool for `tdd` (and `codebase-design` when a new module or interface is involved).
73
+
74
+ **Domain lane.** Call the `skill` tool for `tdd` (and `codebase-design` when a new module or interface is involved).
46
75
  Show one table and go on to step 3 without waiting:
47
76
 
48
77
  | Scenario (exact test title) | Seam | Test type | Test file |
49
78
  |---|---|---|---|
50
79
 
51
- - **Story**: one row per Gherkin scenario. **Tech task**: one row per checklist item that can be tested; the rest gets its evidence named (setting, document). **Spike**: no table – see *Spikes*.
52
- - Seams come from the seam catalog in the engineering conventions. A seam not in the catalog is a decision for the user – the one case where you stop here and ask.
80
+ - One row per Gherkin scenario – at most 4 by the 1-command rule.
81
+ - Seams come from the seam catalog in the engineering conventions. A seam not in the catalog is a decision for the
82
+ user – the one case where you stop here and ask.
53
83
  - Name anything the story leaves open. Don't guess – see *When the story is wrong*.
54
84
 
55
85
  Seams from the catalog are the "confirmed seams" the tdd skill requires; the user approved the catalog.
56
86
 
57
- ## 3. Red → green, one scenario at a time
58
- - Per row: write the test (title exactly `ST-NNN: <scenario title>`), run it and see it fail for the right reason, write the minimal code to pass, run it green. Commit: `ST-NNN: <scenario title>`.
59
- - Domain IDs in the code: commands, events, read models and policies carry their ID from `events.yaml` (`CMD-ReportProblem`, `EVT-ProblemReported`, …). New code uses the words of `CONTEXT.md`; a new term goes into `CONTEXT.md` first (domain-model skill).
60
- - Never weaken or delete an existing test to get green. If a test from an earlier story blocks you, its story and this one contradict each other → *When the story is wrong*.
61
- - Hooks help on the way: formatting and lint after every write, the quick gate (type check + related tests) before you finish a turn.
62
- - In a monorepo, run the affected set for the packages you touched before the full gate:
63
- `node .claude/skills/implement/scripts/affected.ts --changed <files>` (or `--base <defaultBranch>`), then
64
- execute the printed command. It follows shared-library dependents; `verify.ts` still runs everything at the end.
87
+ **Fast lane.** No scenario table. Name the files or packages you will touch and the unit/integration tests that
88
+ cover them; the story's checklist is the acceptance. If it already has scenarios, keep them as a sanity check but do
89
+ not create `ST-NNN:` test titles.
90
+
91
+ ## 2a. Characterization net (domain lane, existing behaviour)
92
+
93
+ Before the first red test, find out whether the affected paths already have a net:
94
+
95
+ ```bash
96
+ node .claude/skills/implement/scripts/characterize.ts --story ST-NNN
97
+ # or name the paths directly
98
+ node .claude/skills/implement/scripts/characterize.ts --changed <file>
99
+ ```
100
+
101
+ The check names the affected source files, the tests it found and the files with **no test found**. It is a
102
+ test-presence check, not a coverage tool: whether a test covers the behaviour that is about to change is your
103
+ judgement.
104
+
105
+ For every affected path whose behaviour is about to change and has no test:
106
+
107
+ 1. Write a **characterization test** that pins the current behaviour (Feathers): assert what the code does today,
108
+ not what the story wants it to do.
109
+ 2. Run it on the unchanged code and see it green. A red characterization test is not the story's red step.
110
+ 3. Commit it separately: `ST-NNN: characterize <area>`.
111
+ 4. Only then start the story's red → green loop (step 3).
112
+
113
+ If the current behaviour contradicts the story, that is a **finding**, not a test to adjust: mark the story `[OPEN]`,
114
+ add a row to `docs/stories/OPEN_QUESTIONS.md`, and send it back with
115
+ `node .claude/skills/implement/scripts/story-status.ts ST-NNN review`. Do not silently make the code fit the story.
116
+
117
+ If the repository has no test runner, stop and report it: setting one up is its own `task` story, not a side effect
118
+ of this one.
119
+
120
+ **Fast lane:** skip this step. The exception is a refactoring – when the task says behaviour stays unchanged (or the
121
+ story carries the label `refactor`), write the characterization net first; a refactor without one is a rewrite.
122
+
123
+ ## 3. Red → green (domain) / implement (fast)
124
+
125
+ **Domain lane:**
126
+ - Per row: write the test (title exactly `ST-NNN: <scenario title>`), run it and see it fail for the right reason,
127
+ write the minimal code to pass, run it green. Commit: `ST-NNN: <scenario title>`.
128
+ - Domain IDs in the code: commands, events, read models and policies carry their ID from `events.yaml`
129
+ (`CMD-ReportProblem`, `EVT-ProblemReported`, …). New code uses the words of `CONTEXT.md`; a new term goes into
130
+ `CONTEXT.md` first (domain-model skill).
131
+ - Never weaken or delete an existing test to get green. If a test from an earlier story blocks you, its story and
132
+ this one contradict each other → *When the story is wrong*.
133
+
134
+ **Fast lane:**
135
+ - Implement the task, keep the change as small as the checklist says, and add or update unit tests where the change
136
+ can break behaviour. No test-title matching, no scenario traceability.
137
+ - Tick each checklist item in the story file as it becomes true (`- [x] …`).
138
+ - If the work turns into domain behaviour (a new/changed command, event or invariant), stop: the story is the wrong
139
+ type. Send it back to `review` as a `domain` story instead of deciding it in code.
140
+
141
+ **Both lanes** – hooks help on the way: formatting and lint after every write, the quick gate (type check + related
142
+ tests) before you finish a turn. In a monorepo, run the affected set for the packages you touched before the full
143
+ gate: `node .claude/skills/implement/scripts/affected.ts --changed <files>` (or `--base <defaultBranch>`), then
144
+ execute the printed command. It follows shared-library dependents; `verify.ts` still runs everything at the end.
65
145
 
66
146
  ## 4. Verify
67
- `node .claude/skills/implement/scripts/verify.ts` (with `--e2e` when browser tests exist) must be green: lint incl. module boundaries, type check, tests, events and stories valid, scenario ↔ test and domain ID traceability. Language warnings: fix or justify.
147
+ `node .claude/skills/implement/scripts/verify.ts` (with `--e2e` when browser tests exist) must be green: lint incl.
148
+ module boundaries, type check, tests, events and stories valid, scenario ↔ test and domain ID traceability. Language
149
+ warnings: fix or justify.
150
+
151
+ The full `--e2e` run happens **once, before the push**. During red → green run only the spec you are working on;
152
+ after the refactor (step 6) run `verify.ts` without `--e2e` plus the specs the refactor touched – CI runs every
153
+ browser test on the pull request anyway (check `e2e`).
154
+
155
+ ## 5. Review (domain lane only, scaled to the diff)
68
156
 
69
- The full `--e2e` run happens **once, before the push**. During red → green run only the spec you are working on; after the refactor (step 6) run `verify.ts` without `--e2e` plus the specs the refactor touched – CI runs every browser test on the pull request anyway (check `e2e`).
157
+ **Fast lane: skip this step.** No `code-reviewer`, no `acceptance-tester`, no review file. The gate is step 4 plus
158
+ the user's merge. If the diff touches authorization, input handling, a migration or security headers, say so and offer
159
+ the normal review anyway – the lane follows the risk, not the label.
70
160
 
71
- ## 5. Review (in parallel, scaled to the story)
72
- Push the branch (`git push -u origin st-NNN-<slug>`) so a preview deployment exists, when the project has one
73
- (`review.preview` in `.claude/project.json`: `vercel`, `netlify`, … – use the provider's MCP or CLI, or the pull
74
- request's preview comment). Otherwise there is no preview and the reviewers work from the code.
161
+ **Domain lane.** Push the branch (`git push -u origin st-NNN-<slug>`) so a preview deployment exists, when the project
162
+ has one (`review.preview` in `.claude/project.json`: `vercel`, `netlify`, … – use the provider's MCP or CLI, or the
163
+ pull request's preview comment). Otherwise there is no preview and the reviewers work from the code.
75
164
 
76
- Pick the lane first – review effort follows the risk of the diff, not a fixed list:
165
+ Pick the lane from the **diff**, not from a size label:
77
166
 
78
- **Light lane** – when **all** of these hold: size **S**, `events: []`, no command, no migration, no input handling
79
- (no request handler, form action or route added or changed), no new dependency. Typical: static pages, texts, a
80
- link, layout.
81
- - Only `acceptance-tester` – task: story ID, base ref, the preview URL (if known), and: *also check the diff briefly against the engineering conventions (texts from the catalogs, the shared page frames, domain language) and list deviations as minors.* Writes `docs/reviews/ST-NNN-acceptance.md`.
167
+ **Light lane** – all of these hold: the diff is texts, layout, styles, links or docs; no command or route added or
168
+ changed; no input handling (no request handler, form action); no migration; no new dependency.
169
+ - Only `acceptance-tester` – task: story ID, base ref, the preview URL (if known), and: *also check the diff briefly
170
+ against the engineering conventions (texts from the catalogs, the shared page frames, domain language) and list
171
+ deviations as minors.* Writes `docs/reviews/ST-NNN-acceptance.md`.
82
172
  - No `code-reviewer`, no extra code or security pass.
83
173
 
84
174
  **Normal lane** – everything else. Start in **one** message:
85
175
  - `code-reviewer` – task: story ID, base ref, the context pack command. Writes `docs/reviews/ST-NNN-code-review.md`.
86
176
  - `acceptance-tester` – task: story ID, base ref, the preview URL (if known). Writes `docs/reviews/ST-NNN-acceptance.md`.
87
- - Only for size **L/XL** or a story that changes the command layer or other shared platform code: ask the `code-reviewer` for a second, bug-focused pass over the diff.
88
- - Only when the **diff** touches input handling, login/roles/session, uploads or files, or security headers/CSP: ask the `code-reviewer` for a security-focused pass.
177
+ - Only when the **diff** touches the command layer or other shared platform code: ask the `code-reviewer` for a
178
+ second, bug-focused pass over the diff.
179
+ - Only when the **diff** touches input handling, login/roles/session, uploads or files, or security headers/CSP: ask
180
+ the `code-reviewer` for a security-focused pass.
89
181
 
90
- Say which lane you picked and why in one line.
182
+ Both reviewers report findings and never create stories. A finding is **fixed in this story** or becomes an open
183
+ question to the developer (step 6) – there is no follow-up ticket.
91
184
 
92
- Tell the reviewers the follow-up hurdle below, so they grade findings with it in mind.
185
+ Say which lane you picked and why in one line.
93
186
 
94
187
  ## 6. Refactor
95
- - Fix all blockers and majors **inside this story**, and the minors you agree with; say which minors you skip and why. Where a finding offers options, apply the recommended one without asking; ask only when no option is recommended or a follow-up story is at stake.
96
- - **Follow-up hurdle** – a finding becomes a new story only if all of these hold:
97
- 1. it is a security or data-loss risk, or it blocks a *named* upcoming story;
98
- 2. it cannot be fixed in this story within about an hour;
99
- 3. no existing story can take it (check the stories that will first touch that code – prefer adding a checklist item there, via `requirements-engineer`, over a new story).
100
- Otherwise it stays a line in the review file ("skipped minor" or "revisit when …") – no story. Say for each follow-up why it passes the hurdle.
101
- - Follow-ups that pass: delegate to `requirements-engineer` as new tech-task stories (`status: draft`, label `follow-up`), referencing the review file. They go through `/review-stories` like any story.
102
- - **Foundation just in time**: technical groundwork goes into the first story that needs it, not into a separate tech task ahead of the domain stories – a `must` tech task with met dependencies jumps ahead of every domain story in the backlog order.
103
- - Refactor only with green tests; the tests must not change unless they were wrong (then say so). Run
104
- `affected.ts` for the touched packages, then `node .claude/skills/implement/scripts/verify.ts` again (without
105
- `--e2e`) plus the browser specs the refactor touched – see step 4.
106
-
107
- ## 7. Acceptance and merge → user checkpoint
108
- The **acceptance tester's verdict is the acceptance**: the story is `done` in the pull request, so the user only merges.
109
- 1. After the refactor, check the verdict in `docs/reviews/ST-NNN-acceptance.md`. `accepted` or `accepted with remarks` → continue. `not accepted` → fix the gaps in the story and run the `acceptance-tester` again, until it accepts. Only when something goes badly wrong – the verdict stays `not accepted` after a second round, a scenario can't be met as written, the preview shows a defect the tester couldn't see – ask the user, the way *When the story is wrong* describes.
110
- 2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN done` – refuses unless every scenario has its test (story) or every checklist item is ticked (spike/tech task) and all dependencies are done. Commit `ST-NNN: done`, push.
188
+ - Fix all blockers and majors **inside this story**, and the minors you agree with; say which minors you skip and why.
189
+ Where a finding offers options, apply the recommended one without asking; ask only when no option is recommended.
190
+ - **No follow-up stories from reviews.** A finding that is too large for this story is either:
191
+ 1. a **question to the developer** in the review file and in the wrap-up (with evidence and options), or
192
+ 2. a line in `## Out of Scope` / a row in `OPEN_QUESTIONS.md` when it is a real decision for the user.
193
+ Only the main session and the user decide whether it becomes a new story; the reviewers never do.
194
+ - Refactor only with green tests; the tests must not change unless they were wrong (then say so). Run `affected.ts`
195
+ for the touched packages, then `node .claude/skills/implement/scripts/verify.ts` again (without `--e2e`) plus the
196
+ browser specs the refactor touched – see step 4.
197
+
198
+ ## 7. Done, acceptance and merge → user checkpoint
199
+
200
+ **Fast lane:** tick the last checklist item, then
201
+ `node .claude/skills/implement/scripts/story-status.ts ST-NNN done` (it refuses while a checklist item is open).
202
+ Commit `ST-NNN: done`, push, open the pull request.
203
+
204
+ **Domain lane:** the **acceptance tester's verdict is the acceptance**: the story is `done` in the pull request, so
205
+ the user only merges.
206
+ 1. After the refactor, check the verdict in `docs/reviews/ST-NNN-acceptance.md`. `accepted` or `accepted with
207
+ remarks` → continue. `not accepted` → fix the gaps in the story and run the `acceptance-tester` again, until it
208
+ accepts. Only when something goes badly wrong – the verdict stays `not accepted` after a second round, a scenario
209
+ can't be met as written, the preview shows a defect the tester couldn't see – ask the user, the way *When the story
210
+ is wrong* describes.
211
+ 2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN done` – refuses unless every scenario has its test
212
+ (domain) or every checklist item is ticked (chore/task/tech task/spike) and all dependencies are done. Commit
213
+ `ST-NNN: done`, push.
111
214
  - GitHub: the status change closes the linked issue.
112
- - Jira: `story-status.ts` prints the transition it could not make (MCP is agent-only). Run `jira_get_transitions` +
113
- `jira_transition_issue` on the printed key, then push the story body once (`/backlog-sync push`).
114
- 3. Open a pull request: title `ST-NNN: <story title>`, body = the scenarios as a checklist, links to the story and both review files, the acceptance verdict, the preview URL, remaining minors, and anything the user must approve (a conventions change). (No `gh` CLI, or it isn't authenticated: push and give the user the compare URL `git push` prints.)
115
- 4. **The user merges** – on the preview if they want to look first. Afterwards: back to the default branch and `git pull`.
215
+ - Jira: `story-status.ts` prints the transition it could not make (MCP is agent-only). Run
216
+ `jira_get_transitions` + `jira_transition_issue` on the printed key, then push the story body once
217
+ (`/backlog-sync push`).
218
+
219
+ **Both lanes – the pull request:**
220
+ 3. Title `ST-NNN: <story title>`, body = the scenarios as a checklist (domain) or the task checklist (fast lane),
221
+ links to the story and the review files (if any), the acceptance verdict (domain), the preview URL, remaining
222
+ minors/questions, and anything the user must approve (a conventions change). (No `gh` CLI, or it isn't
223
+ authenticated: push and give the user the compare URL `git push` prints.)
224
+ 4. **The user merges** – on the preview if they want to look first. Afterwards: back to the default branch and
225
+ `git pull`.
116
226
 
117
227
  ## 8. Wrap-up
118
- Summarize in a few lines: scenarios implemented, review findings fixed/skipped, follow-ups created, glossary/ADR changes. Then:
119
- - Suggest `/improve-codebase-architecture` after the foundation stories, when a bounded context is complete, or about every 8–10 done stories.
120
- - Name the next story (`node .claude/skills/implement/scripts/next-story.ts`) and tell the user to start it in a fresh session (`/new`, then `/implement`): every turn re-reads the whole session, so a second story in the same one costs several times the first.
228
+ Summarize in a few lines: lane taken, scenarios implemented or task completed, review findings fixed/skipped, open
229
+ questions handed to the developer, glossary/ADR changes. Then:
230
+ - Suggest `/improve-codebase-architecture` after the foundation stories, when a bounded context is complete, or about
231
+ every 8–10 done stories.
232
+ - Name the next story (`node .claude/skills/implement/scripts/next-story.ts`) and tell the user to start it in a fresh
233
+ session (`/new`, then `/implement`): every turn re-reads the whole session, so a second story in the same one costs
234
+ several times the first.
121
235
 
122
236
  ## Spikes
123
237
  A spike answers its **Question** within its **Timebox** – no test plan, no TDD for throwaway experiments.
124
- - A UX or state-model spike is a prototype: load the `prototype` skill, keep the question and timebox in the
125
- story, and capture the verdict the same way (step 1b).
126
- - Before starting, tell the user the timebox and what will be kept (skeleton code that becomes the application gets tests and passes verify).
127
- - Tick each checklist item in the story file as it is answered; write results where the item says (an ADR's consequences, `OPEN_QUESTIONS.md`). A new decision → ADR with `status: proposed` (architect skill); the user accepts.
238
+ - A UX or state-model spike is a prototype: load the `prototype` skill, keep the question and timebox in the story,
239
+ and capture the verdict the same way (step 1b).
240
+ - Before starting, tell the user the timebox and what will be kept (skeleton code that becomes the application gets
241
+ tests and passes verify).
242
+ - Tick each checklist item in the story file as it is answered; write results where the item says (an ADR's
243
+ consequences, `OPEN_QUESTIONS.md`). A new decision → ADR with `status: proposed` (architect skill); the user accepts.
128
244
  - Timebox exhausted → stop, document what is known, and ask the user how to continue.
129
245
  - Review: `code-reviewer` only for code that is kept; `acceptance-tester` checks the checklist evidence.
130
246
 
131
247
  ## When the story is wrong
132
- Never guess. When a scenario contradicts another story, a rule or an ADR, can't be tested as written, or leaves a decision open:
248
+ Never guess. When a scenario contradicts another story, a rule or an ADR, can't be tested as written, or leaves a
249
+ decision open – and when a characterization test shows that the current behaviour differs from what the story
250
+ assumes:
133
251
  1. Stop implementing that part. Mark it `[OPEN]` in the story and add a row to `docs/stories/OPEN_QUESTIONS.md`.
134
- 2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN review` and tell the user. The fix goes through discovery: `requirements-engineer` revises the story, `/review-stories` approves it again. Work already done on the branch stays.
135
- 3. New domain term → `CONTEXT.md` (domain-model skill). Architecture deviation → a `proposed` ADR (architect skill). An accepted ADR is never edited; the hooks block it.
252
+ 2. `node .claude/skills/implement/scripts/story-status.ts ST-NNN review` and tell the user. The fix goes through
253
+ discovery in the main session: revise the story, run the spec linter, and let the user approve it again
254
+ (`/review-stories`). Work already done on the branch stays.
255
+ 3. New domain term → `CONTEXT.md` (domain-model skill). Architecture deviation → a `proposed` ADR (architect skill).
256
+ An accepted ADR is never edited; the hooks block it.
136
257
  4. A `done` story is never reopened – a change to finished behaviour is a new story.
137
258
 
138
259
  ## Foundation stories
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Finds the source files a story or a diff touches and checks whether a test exists for each of them.
3
+ *
4
+ * node .claude/skills/implement/scripts/characterize.ts --story ST-012
5
+ * node .claude/skills/implement/scripts/characterize.ts --changed src/services/order-service.ts
6
+ * node .claude/skills/implement/scripts/characterize.ts --base origin/main --json
7
+ *
8
+ * This is a deterministic *test-presence* check, not a coverage tool: it finds tests by file-name convention
9
+ * and by imports/references of the source. Whether a test covers the behaviour that is about to change is the
10
+ * agent's judgement. When no test exists, /implement writes characterization tests first and commits them
11
+ * separately, before the story's red → green loop.
12
+ *
13
+ * Exit 0 unless the arguments are wrong or the story does not exist; a missing test is a finding, not a crash.
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { basename, extname, join } from "node:path";
17
+ import * as d from "../../../lib/discovery.ts";
18
+ import * as e from "../../../lib/engineering.ts";
19
+ import { readRepoMap, scanRepo, type RepoPackage } from "../../../lib/repo.ts";
20
+ import { packageForFile } from "../../../lib/slices.ts";
21
+
22
+ const args = process.argv.slice(2);
23
+
24
+ function usage(): void {
25
+ console.log(
26
+ [
27
+ "Usage: node .claude/skills/implement/scripts/characterize.ts [options]",
28
+ "",
29
+ " --story <ST-NNN> find source files from the story's IDs, names and terms",
30
+ " --changed <file> a file (repeatable or comma-separated); default: git status",
31
+ " --base <ref> use the files changed against this ref",
32
+ " --json machine-readable result",
33
+ ].join("\n"),
34
+ );
35
+ }
36
+
37
+ if (args.includes("--help") || args.includes("-h")) {
38
+ usage();
39
+ process.exit(0);
40
+ }
41
+
42
+ let storyId = "";
43
+ const changed: string[] = [];
44
+ let base = "";
45
+ for (let i = 0; i < args.length; i += 1) {
46
+ const take = (): string | undefined => {
47
+ const value = args[i + 1];
48
+ if (!value || value.startsWith("--")) return undefined;
49
+ i += 1;
50
+ return value;
51
+ };
52
+ if (args[i] === "--story") storyId = (take() ?? "").toUpperCase();
53
+ else if (args[i] === "--changed") {
54
+ const value = take();
55
+ if (value) changed.push(...value.split(",").map((f) => f.trim()).filter(Boolean));
56
+ } else if (args[i] === "--base") base = take() ?? base;
57
+ }
58
+
59
+ if (!storyId && !changed.length && !base) {
60
+ usage();
61
+ process.exit(1);
62
+ }
63
+
64
+ const list = (v: unknown): string[] => (Array.isArray(v) ? v : []);
65
+ const read = (file: string): string => {
66
+ try {
67
+ return readFileSync(join(d.ROOT, file), "utf8");
68
+ } catch {
69
+ return "";
70
+ }
71
+ };
72
+
73
+ const map = readRepoMap(d.ROOT) ?? scanRepo(d.ROOT, { toolGraph: false });
74
+ const pkgFor = (file: string): RepoPackage | undefined => packageForFile(map.packages, file);
75
+
76
+ const testFiles = e.projectFiles().filter(e.isTest);
77
+ const sourceFiles = e.projectFiles().filter((f) => e.isCode(f) && !e.isTestCode(f));
78
+
79
+ function changedFiles(): string[] {
80
+ if (changed.length) return [...new Set(changed)];
81
+ if (base) return (e.git("diff", "--name-only", base) ?? "").split("\n").map((l) => l.trim()).filter(Boolean);
82
+ return (e.git("status", "--porcelain", "--untracked-files=all") ?? "")
83
+ .split("\n")
84
+ .map((line) => line.slice(3).trim().replace(/^.* -> /, ""))
85
+ .filter(Boolean);
86
+ }
87
+
88
+ function testBase(file: string): string {
89
+ return basename(file, extname(file))
90
+ .replace(/^test_/, "")
91
+ .replace(/(?:[._-](?:test|spec|tests|specs))$/i, "")
92
+ .replace(/(?:Test|Tests|Spec|Specs|IT)$/i, "")
93
+ .toLowerCase();
94
+ }
95
+
96
+ function testsFor(source: string): { file: string; reason: string }[] {
97
+ const sourceBase = basename(source, extname(source)).toLowerCase();
98
+ const out: { file: string; reason: string }[] = [];
99
+ for (const test of testFiles) {
100
+ const reason: string[] = [];
101
+ if (testBase(test) === sourceBase) reason.push("same name");
102
+ const text = read(test);
103
+ // A reference to the module is enough evidence to look; it is not proof of behavioural coverage.
104
+ if (sourceBase.length >= 3 && text.toLowerCase().includes(sourceBase)) reason.push("mentions the module");
105
+ if (reason.length) out.push({ file: test, reason: reason.join(", ") });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ function storySeedTerms(story: d.Story): { ids: string[]; names: string[]; words: string[] } {
111
+ const ids = new Set<string>();
112
+ const names = new Set<string>();
113
+ const text = `${story.meta.title ?? ""}\n${story.body}`;
114
+ for (const id of text.matchAll(/\b(?:ACT|EXT|BC|AGG|EVT|CMD|POL|RM)-[A-Za-z0-9]+\b/g)) ids.add(id[0]);
115
+ for (const key of ["aggregate", "command", "context"]) {
116
+ if (typeof story.meta[key] === "string") ids.add(story.meta[key]);
117
+ }
118
+ for (const id of list(story.meta.events)) ids.add(id);
119
+
120
+ const model = d.loadEventsModel();
121
+ if (model) {
122
+ const idx = d.indexModel(model);
123
+ for (const id of [...ids]) {
124
+ const item = idx.get(id);
125
+ if (typeof item?.name === "string") names.add(item.name);
126
+ }
127
+ for (const command of (model.commands ?? []) as d.Obj[]) {
128
+ if (list(command.produces).some((event) => list(story.meta.events).includes(event))) {
129
+ if (typeof command.name === "string") names.add(command.name);
130
+ }
131
+ }
132
+ }
133
+
134
+ const glossary = e.loadGlossary();
135
+ for (const term of glossary.terms) {
136
+ if (new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(text)) names.add(term);
137
+ }
138
+
139
+ const stop = new Set(["story", "want", "that", "with", "from", "this", "when", "then", "given", "should", "must", "can", "the", "and", "for"]);
140
+ const words = [...new Set(
141
+ String(story.meta.title ?? "")
142
+ .toLowerCase()
143
+ .split(/[^a-z0-9]+/)
144
+ .filter((word) => word.length >= 4 && !stop.has(word)),
145
+ )];
146
+ return { ids: [...ids], names: [...names], words };
147
+ }
148
+
149
+ function scoreSource(file: string, terms: { ids: string[]; names: string[]; words: string[] }): { score: number; reasons: string[] } {
150
+ const text = read(file).toLowerCase();
151
+ const path = file.toLowerCase();
152
+ let score = 0;
153
+ const reasons: string[] = [];
154
+ for (const id of terms.ids) {
155
+ if (text.includes(id.toLowerCase())) {
156
+ score += 3;
157
+ reasons.push(`mentions ${id}`);
158
+ }
159
+ }
160
+ for (const name of terms.names) {
161
+ const lower = name.toLowerCase();
162
+ if (lower.length >= 3 && new RegExp(`\\b${lower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text)) {
163
+ score += 2;
164
+ reasons.push(`mentions "${name}"`);
165
+ }
166
+ }
167
+ for (const word of terms.words) {
168
+ if (path.includes(word)) {
169
+ score += 1;
170
+ reasons.push(`path contains "${word}"`);
171
+ }
172
+ }
173
+ return { score, reasons };
174
+ }
175
+
176
+ let story: d.Story | undefined;
177
+ const seeds = new Map<string, { file: string; score: number; reasons: string[] }>();
178
+ const warnings: string[] = [];
179
+
180
+ if (storyId) {
181
+ try {
182
+ story = e.findStory(storyId);
183
+ } catch (error) {
184
+ console.error(error instanceof Error ? error.message : String(error));
185
+ process.exit(1);
186
+ }
187
+ const terms = storySeedTerms(story);
188
+ for (const file of sourceFiles) {
189
+ const scored = scoreSource(file, terms);
190
+ if (scored.score > 0) seeds.set(file, { file, ...scored });
191
+ }
192
+ if (!seeds.size) warnings.push("no source file mentions the story's IDs or terms – pass --changed to name the affected paths");
193
+ }
194
+
195
+ for (const file of changedFiles()) {
196
+ if (!existsSync(join(d.ROOT, file))) continue;
197
+ if (!sourceFiles.includes(file)) continue;
198
+ seeds.set(file, { file, score: seeds.get(file)?.score ?? 0, reasons: [...(seeds.get(file)?.reasons ?? []), "changed"] });
199
+ }
200
+
201
+ const sourceRows = [...seeds.values()]
202
+ .sort((a, b) => b.score - a.score || a.file.localeCompare(b.file))
203
+ .slice(0, 40)
204
+ .map((seed) => {
205
+ const tests = testsFor(seed.file);
206
+ const pkg = pkgFor(seed.file);
207
+ return {
208
+ file: seed.file,
209
+ package: pkg?.path,
210
+ score: seed.score,
211
+ reasons: seed.reasons,
212
+ tests,
213
+ covered: tests.length > 0,
214
+ confidence: tests.some((test) => test.reason.includes("same name") || test.reason.includes("mentions the module")) ? "medium" : "low",
215
+ };
216
+ });
217
+
218
+ const storyTests = storyId
219
+ ? testFiles.filter((file) => read(file).includes(storyId)).map((file) => ({ file, reason: `mentions ${storyId}` }))
220
+ : [];
221
+ if (storyId && !storyTests.length) warnings.push(`no test mentions ${storyId} yet – expected before the story's red → green loop`);
222
+
223
+ const result = {
224
+ story: story ? { id: storyId, title: story.meta.title, status: story.meta.status, type: story.meta.type } : null,
225
+ seeds: [...seeds.values()].sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, 40),
226
+ sources: sourceRows,
227
+ storyTests,
228
+ warnings,
229
+ };
230
+
231
+ if (args.includes("--json")) {
232
+ console.log(JSON.stringify(result, null, 2));
233
+ process.exit(0);
234
+ }
235
+
236
+ console.log(`Characterization check${story ? ` – ${storyId}: ${story.meta.title}` : ""}`);
237
+ if (!sourceRows.length) {
238
+ console.log("\nNo affected source file found. Name the paths with --changed, or widen the story terms.");
239
+ }
240
+ for (const row of sourceRows) {
241
+ const status = row.covered ? "test found" : "NO TEST FOUND";
242
+ console.log(`\n- ${row.file} (${row.package ?? "unmapped"}) – ${status}`);
243
+ if (row.reasons.length) console.log(` why: ${row.reasons.join("; ")}`);
244
+ for (const test of row.tests) console.log(` test: ${test.file} (${test.reason})`);
245
+ if (!row.covered) console.log(" → write a characterization test first; pin the current behaviour, commit it separately, then start the story tests.");
246
+ }
247
+ if (storyTests.length) {
248
+ console.log(`\nTests that mention ${storyId}:`);
249
+ for (const test of storyTests) console.log(`- ${test.file} (${test.reason})`);
250
+ }
251
+ for (const warning of warnings) console.log(`\nWARNING ${warning}`);
252
+ console.log("\nA test file is evidence, not proof of behavioural coverage. If the current behaviour contradicts the story, the story goes back to review.");
@@ -36,7 +36,7 @@ for (const [id, scenarios] of superseded) {
36
36
  }
37
37
 
38
38
  for (const [id, s] of stories) {
39
- if (only ? id !== only : !["in-progress", "done"].includes(s.meta.status) || s.meta.type !== "story") continue;
39
+ if (only ? id !== only : !["in-progress", "done"].includes(s.meta.status) || !d.isDomain(s.meta)) continue;
40
40
  const { covered, missing: untested } = e.scenarioCoverage(s, titles);
41
41
  const missing = untested.filter((sc) => !superseded.get(id)?.has(sc));
42
42
  for (const sc of missing) {