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.
- package/.claude/THIRD_PARTY_NOTICES.md +4 -0
- package/.claude/agents/acceptance-tester.md +7 -2
- package/.claude/agents/code-reviewer.md +7 -2
- package/.claude/agents/domain-architect.md +7 -3
- package/.claude/agents/researcher.md +77 -0
- package/.claude/agents/ux-designer.md +42 -47
- package/.claude/hooks/validate-docs.ts +17 -1
- package/.claude/lib/architecture.ts +3 -33
- package/.claude/lib/backlog.ts +2 -0
- package/.claude/lib/config.ts +49 -0
- package/.claude/lib/discovery.ts +200 -32
- package/.claude/lib/engineering.ts +18 -6
- package/.claude/lib/gaps.ts +179 -0
- package/.claude/lib/glob.ts +39 -0
- package/.claude/lib/repo.ts +143 -19
- package/.claude/lib/research.ts +303 -0
- package/.claude/lib/slices.ts +529 -0
- package/.claude/package.json +1 -0
- package/.claude/settings.json +2 -2
- package/.claude/skills/adopt/SKILL.md +111 -34
- package/.claude/skills/adopt/scripts/gap-register.ts +61 -0
- package/.claude/skills/adopt/scripts/repo-map.ts +34 -2
- package/.claude/skills/adopt/scripts/slice-doc.ts +117 -0
- package/.claude/skills/adopt/scripts/slice.ts +163 -0
- package/.claude/skills/adopt/templates/context-map.as-is.md +22 -0
- package/.claude/skills/architect/SKILL.md +6 -1
- package/.claude/skills/backlog/SKILL.md +2 -2
- package/.claude/skills/bootstrap/SKILL.md +9 -7
- package/.claude/skills/domain-model/CONTEXT-FORMAT.md +1 -1
- package/.claude/skills/domain-model/SKILL.md +24 -4
- package/.claude/skills/event-storming/SKILL.md +3 -2
- package/.claude/skills/event-storming/schema/events.schema.json +2 -0
- package/.claude/skills/event-storming/templates/events.example.yaml +6 -0
- package/.claude/skills/feature/SKILL.md +16 -13
- package/.claude/skills/feature/scripts/intake.ts +1 -1
- package/.claude/skills/grilling/SKILL.md +15 -0
- package/.claude/skills/groom-backlog/SKILL.md +19 -17
- package/.claude/skills/implement/SKILL.md +199 -78
- package/.claude/skills/implement/scripts/characterize.ts +252 -0
- package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
- package/.claude/skills/implement/scripts/story-context.ts +65 -1
- package/.claude/skills/implement/scripts/verify.ts +4 -1
- package/.claude/skills/improve-codebase-architecture/SKILL.md +4 -1
- package/.claude/skills/prototype/SKILL.md +1 -1
- package/.claude/skills/research/SKILL.md +149 -0
- package/.claude/skills/research/scripts/research-check.ts +105 -0
- package/.claude/skills/research/templates/research.md +29 -0
- package/.claude/skills/review-stories/SKILL.md +49 -19
- package/.claude/skills/shape/SKILL.md +26 -7
- package/.claude/skills/tdd/SKILL.md +10 -0
- package/.claude/skills/user-stories/SKILL.md +128 -51
- package/.claude/skills/user-stories/schema/story.schema.json +10 -3
- package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
- package/.claude/skills/user-stories/templates/story-template.md +6 -2
- package/.claude/skills/ux-design/SKILL.md +9 -7
- package/.opencode/agents/acceptance-tester.md +8 -3
- package/.opencode/agents/code-reviewer.md +8 -3
- package/.opencode/agents/domain-architect.md +7 -3
- package/.opencode/agents/researcher.md +91 -0
- package/.opencode/agents/ux-designer.md +42 -52
- package/.opencode/commands/adopt.md +1 -1
- 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/research.md +7 -0
- package/.opencode/commands/review-stories.md +1 -1
- package/.opencode/commands/shape.md +1 -1
- package/AGENTS.md +30 -0
- package/CHANGELOG.md +73 -0
- package/README.md +108 -24
- package/guide/brownfield.md +41 -12
- package/guide/concepts.md +23 -6
- package/guide/greenfield.md +4 -3
- package/package.json +1 -1
- package/src/cli.mjs +42 -4
- package/src/detect.mjs +9 -0
- package/src/install.mjs +66 -6
- package/templates/docs/domain/context-map.md +5 -1
- package/templates/docs/domain/events.yaml +3 -0
- package/templates/docs/research/README.md +70 -0
- package/templates/docs/stories/README.md +12 -3
- package/templates/docs/workflow.md +76 -19
- package/.claude/agents/lead-dev.md +0 -52
- package/.claude/agents/product-owner.md +0 -39
- package/.claude/agents/requirements-engineer.md +0 -44
- package/.opencode/agents/lead-dev.md +0 -69
- package/.opencode/agents/product-owner.md +0 -59
- 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/
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
+
## Two lanes
|
|
15
21
|
|
|
16
|
-
|
|
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
|
|
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,
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
3. **Capture.** Commit `docs/prototypes/<slug>/` (or the in-app prototype) on `prototype/<slug>`, return to the
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
4. **Then decide.** If the prototype only settles layout and the scenarios still hold, continue with step 2.
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
52
|
-
- Seams come from the seam catalog in the engineering conventions. A seam not in the catalog is a decision for the
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
165
|
+
Pick the lane from the **diff**, not from a size label:
|
|
77
166
|
|
|
78
|
-
**Light lane** –
|
|
79
|
-
(no request handler, form action
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
88
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
113
|
-
`jira_transition_issue` on the printed key, then push the story body once
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
119
|
-
|
|
120
|
-
-
|
|
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
|
-
|
|
126
|
-
- Before starting, tell the user the timebox and what will be kept (skeleton code that becomes the application gets
|
|
127
|
-
|
|
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
|
|
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
|
|
135
|
-
|
|
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
|
|
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) {
|