Skip to content

Commit ffc943a

Browse files
authored
chore(workflow): integrate pytest-beehave and clean up @id conventions (#78)
* chore(workflow): integrate pytest-beehave and clean up @id conventions - Add pytest-beehave[html]>=3.0 to dev deps; configure [tool.beehave] - Fix test-build task: --cov=pytest_beehave -> --cov=app - Remove manual deprecated skip hook from conftest.py (beehave owns it) - Naming: feature-stem for .feature paths, feature_slug for test dirs - @id tags now auto-assigned on first pytest run; remove all manual generation instructions - Test stubs auto-generated at Step 2 end via test-fast; remove manual stub section - Add pytest-beehave to README tooling table; add Why section and auto-gen stub example - Update product-owner.md, AGENTS.md, and all affected skills accordingly * chore(workflow): number self-declaration items and add completeness enforcement - implementation/SKILL.md: number all 25 items 1-25; add count reminder comment - verify/SKILL.md: add completeness hard gate (count must be 25, sequence must be gapless); expand report table from 21 to 25 numbered rows matching implementation template exactly
1 parent 633e701 commit ffc943a

File tree

16 files changed

+198
-158
lines changed

16 files changed

+198
-158
lines changed

.opencode/agents/product-owner.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,18 @@ When a gap is reported (by software-engineer or reviewer):
5151

5252
| Situation | Action |
5353
|---|---|
54-
| Edge case within current user stories | Add a new Example with a new `@id` to the relevant `.feature` file. |
54+
| Edge case within current user stories | Add a new Example to the relevant `.feature` file. |
5555
| New behavior beyond current stories | Add to backlog as a new feature. Do not extend the current feature. |
56-
| Behavior contradicts an existing Example | Write a new Example with new `@id`. |
57-
| Post-merge defect | Move the `.feature` file back to `in-progress/`, add new Example with `@id`, resume at Step 3. |
56+
| Behavior contradicts an existing Example | Add `@deprecated` to the old Example; write a new Example. |
57+
| Post-merge defect | Move the `.feature` file back to `in-progress/`, add new Example, resume at Step 3. |
5858

5959
## Bug Handling
6060

6161
When a defect is reported against any feature:
6262

63-
1. Add a `@bug @id:<new-8-char-hex>` Example to the relevant `Rule:` block in the `.feature` file.
64-
2. Write the Example using the standard `Given/When/Then` format describing the correct behavior.
65-
3. Update TODO.md to note the new `@id` for the SE to implement.
66-
4. SE implements the `@id` test in `tests/features/` **and** a `@given` Hypothesis property test in `tests/unit/`. Both are required.
63+
1. Add a `@bug` Example to the relevant `Rule:` block in the `.feature` file using the standard `Given/When/Then` format describing the correct behavior.
64+
2. Update TODO.md to note the new bug Example for the SE to implement.
65+
3. SE implements the test in `tests/features/` **and** a `@given` Hypothesis property test in `tests/unit/`. Both are required.
6766

6867
## Available Skills
6968

.opencode/skills/implementation/SKILL.md

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,12 @@ Place stubs where responsibility dictates — do not pre-create `ports/` or `ada
116116
Append a new dated block to `docs/architecture.md` for each significant decision:
117117

118118
```markdown
119-
## YYYY-MM-DD — <feature-name>: <short title>
119+
## YYYY-MM-DD — <feature-stem>: <short title>
120120

121121
Decision: <what was decided>
122122
Reason: <why, one sentence>
123123
Alternatives considered: <what was rejected and why>
124-
Feature: <feature-name>
124+
Feature: <feature-stem>
125125
```
126126

127127
Only write a block for non-obvious decisions with meaningful trade-offs. Routine YAGNI choices do not need a record.
@@ -141,7 +141,11 @@ Apply to the stub files just written:
141141

142142
If any check fails: fix the stub files before committing.
143143

144-
Commit: `feat(<feature-name>): add architecture stubs`
144+
### Generate Test Stubs
145+
146+
Run `uv run task test-fast` once. It reads the in-progress `.feature` file, assigns `@id` tags to any untagged `Example:` blocks (writing them back to the `.feature` file), and generates `tests/features/<feature_slug>/<rule_slug>_test.py` — one file per `Rule:` block, one skipped function per `@id`. Verify the files were created, then stage all changes (including any `@id` write-backs to the `.feature` file).
147+
148+
Commit: `feat(<feature-stem>): add architecture and test stubs`
145149

146150
---
147151

@@ -152,26 +156,14 @@ Commit: `feat(<feature-name>): add architecture stubs`
152156
- [ ] Exactly one .feature `in_progress`. If not present, Load `skill feature-selection`
153157
- [ ] Architecture stubs present in `<package>/` (committed by Step 2)
154158
- [ ] Read `docs/architecture.md` — understand all architectural decisions before writing any test
155-
- [ ] Test stub files exist in `tests/features/<feature-name>/<rule_slug>_test.py` — one file per `Rule:` block, all `@id` stub functions present with `@pytest.mark.skip`; if missing, write them now before entering RED
156-
157-
### Write Test Stubs (if not present)
158-
159-
For each `Rule:` block in the in-progress `.feature` file, create `tests/features/<feature-name>/<rule_slug>_test.py` if it does not already exist. Write one function per `@id` Example, all skipped:
160-
161-
```python
162-
@pytest.mark.skip(reason="not yet implemented")
163-
def test_<feature_slug>_<@id>() -> None:
164-
"""
165-
<@id steps raw text including new lines>
166-
"""
167-
```
159+
- [ ] Test stub files exist in `tests/features/<feature_slug>/<rule_slug>_test.py` — generated by pytest-beehave at Step 2 end; if missing, re-run `uv run task test-fast` and commit the generated files before entering RED
168160

169161
### Build TODO.md Test List
170162

171163
1. List all `@id` tags from in-progress `.feature` file
172164
2. Order: fewest dependencies first; most impactful within that set
173165
3. Each `@id` = one TODO item, status: `pending`
174-
4. Confirm each `@id` has a corresponding skipped stub in `tests/features/<feature-name>/` — if any are missing, add them before proceeding
166+
4. Confirm each `@id` has a corresponding skipped stub in `tests/features/<feature_slug>/` — if any are missing, add them before proceeding
175167

176168
### Outer Loop — One @id at a time
177169

@@ -182,7 +174,7 @@ For each pending `@id`:
182174
```
183175
INNER LOOP
184176
├── RED
185-
│ ├── Confirm stub for this @id exists in tests/features/<feature-name>/<rule_slug>.feature with @pytest.mark.skip
177+
│ ├── Confirm stub for this @id exists in tests/features/<feature_slug>/<rule_slug>_test.py with @pytest.mark.skip
186178
│ ├── Read existing stubs in `<package>/` — base the test on the current data model and signatures
187179
│ ├── Write test body (Given/When/Then → Arrange/Act/Assert); remove @pytest.mark.skip
188180
│ ├── Update <package> stub signatures as needed — edit the `.py` file directly
@@ -221,33 +213,35 @@ All must pass before Self-Declaration.
221213

222214
### Self-Declaration (once, after all quality gates pass)
223215

216+
<!-- This list has exactly 25 items — count before submitting. If your count ≠ 25, you missed one. -->
217+
224218
Communicate verbally to the reviewer. Answer honestly for each principle:
225219

226-
- YAGNI: no code without a failing test — AGREE/DISAGREE | file:line
227-
- YAGNI: no speculative abstractions — AGREE/DISAGREE | file:line
228-
- KISS: simplest solution that passes — AGREE/DISAGREE | file:line
229-
- KISS: no premature optimization — AGREE/DISAGREE | file:line
230-
- DRY: no duplication — AGREE/DISAGREE | file:line
231-
- DRY: no redundant comments — AGREE/DISAGREE | file:line
232-
- SOLID-S: one reason to change per class — AGREE/DISAGREE | file:line
233-
- SOLID-O: open for extension, closed for modification — AGREE/DISAGREE | file:line
234-
- SOLID-L: subtypes substitutable — AGREE/DISAGREE | file:line
235-
- SOLID-I: no forced unused deps — AGREE/DISAGREE | file:line
236-
- SOLID-D: depend on abstractions, not concretions — AGREE/DISAGREE | file:line
237-
- OC-1: one level of indentation per method — AGREE/DISAGREE | deepest: file:line
238-
- OC-2: no else after return — AGREE/DISAGREE | file:line
239-
- OC-3: primitive types wrapped — AGREE/DISAGREE | file:line
240-
- OC-4: first-class collections — AGREE/DISAGREE | file:line
241-
- OC-5: one dot per line — AGREE/DISAGREE | file:line
242-
- OC-6: no abbreviations — AGREE/DISAGREE | file:line
243-
- OC-7: ≤20 lines per function, ≤50 per class — AGREE/DISAGREE | longest: file:line
244-
- OC-8: ≤2 instance variables per class (behavioural classes only; dataclasses, Pydantic models, value objects, and TypedDicts are exempt) — AGREE/DISAGREE | file:line
245-
- OC-9: no getters/setters — AGREE/DISAGREE | file:line
246-
- Patterns: I have no good reason to refactor parts of the code using OOP or Design Patterns — AGREE/DISAGREE | file:line
247-
- Patterns: no creational smell — AGREE/DISAGREE | file:line
248-
- Patterns: no structural smell — AGREE/DISAGREE | file:line
249-
- Patterns: no behavioral smell — AGREE/DISAGREE | file:line
250-
- Semantic: tests operate at same abstraction as AC — AGREE/DISAGREE | file:line
220+
1. YAGNI: no code without a failing test — AGREE/DISAGREE | file:line
221+
2. YAGNI: no speculative abstractions — AGREE/DISAGREE | file:line
222+
3. KISS: simplest solution that passes — AGREE/DISAGREE | file:line
223+
4. KISS: no premature optimization — AGREE/DISAGREE | file:line
224+
5. DRY: no duplication — AGREE/DISAGREE | file:line
225+
6. DRY: no redundant comments — AGREE/DISAGREE | file:line
226+
7. SOLID-S: one reason to change per class — AGREE/DISAGREE | file:line
227+
8. SOLID-O: open for extension, closed for modification — AGREE/DISAGREE | file:line
228+
9. SOLID-L: subtypes substitutable — AGREE/DISAGREE | file:line
229+
10. SOLID-I: no forced unused deps — AGREE/DISAGREE | file:line
230+
11. SOLID-D: depend on abstractions, not concretions — AGREE/DISAGREE | file:line
231+
12. OC-1: one level of indentation per method — AGREE/DISAGREE | deepest: file:line
232+
13. OC-2: no else after return — AGREE/DISAGREE | file:line
233+
14. OC-3: primitive types wrapped — AGREE/DISAGREE | file:line
234+
15. OC-4: first-class collections — AGREE/DISAGREE | file:line
235+
16. OC-5: one dot per line — AGREE/DISAGREE | file:line
236+
17. OC-6: no abbreviations — AGREE/DISAGREE | file:line
237+
18. OC-7: ≤20 lines per function, ≤50 per class — AGREE/DISAGREE | longest: file:line
238+
19. OC-8: ≤2 instance variables per class (behavioural classes only; dataclasses, Pydantic models, value objects, and TypedDicts are exempt) — AGREE/DISAGREE | file:line
239+
20. OC-9: no getters/setters — AGREE/DISAGREE | file:line
240+
21. Patterns: no good reason remains to refactor using OOP or Design Patterns — AGREE/DISAGREE | file:line
241+
22. Patterns: no creational smell — AGREE/DISAGREE | file:line
242+
23. Patterns: no structural smell — AGREE/DISAGREE | file:line
243+
24. Patterns: no behavioral smell — AGREE/DISAGREE | file:line
244+
25. Semantic: tests operate at same abstraction as AC — AGREE/DISAGREE | file:line
251245

252246
A `DISAGREE` answer is not automatic rejection — state the reason and fix before handing off.
253247

@@ -265,11 +259,11 @@ Signal completion to the reviewer. Provide:
265259
### Test File Layout
266260

267261
```
268-
tests/features/<feature-name>/<rule_slug>_test.py
262+
tests/features/<feature_slug>/<rule_slug>_test.py
269263
```
270264

271-
- `<feature-name>` = the `.feature` file stem
272-
- `<rule_slug>` = the `Rule:` title slugified
265+
- `<feature_slug>` = the `.feature` file stem with hyphens replaced by underscores, lowercase
266+
- `<rule_slug>` = the `Rule:` title slugified (lowercase, underscores)
273267

274268
### Function Naming
275269

@@ -299,7 +293,7 @@ def test_<feature_slug>_<@id>() -> None:
299293
### Markers
300294

301295
- `@pytest.mark.slow` — takes > 50ms (Hypothesis, DB, network, terminal I/O)
302-
- `@pytest.mark.deprecated` — auto-skipped by conftest; used for superseded Examples
296+
- `@pytest.mark.deprecated` — auto-skipped by pytest-beehave; used for superseded Examples
303297

304298
```python
305299
@pytest.mark.deprecated

.opencode/skills/living-docs/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ If `docs/glossary.md` already exists:
188188
**When run standalone** (stakeholder on demand): commit after all diagrams and glossary are updated:
189189

190190
```
191-
docs(living-docs): update C4 and glossary after <feature-name>
191+
docs(living-docs): update C4 and glossary after <feature-stem>
192192
```
193193

194194
If triggered without a specific feature (general refresh):

.opencode/skills/pr-management/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Create and manage pull requests after the reviewer approves the feature (Step 5)
1414
## Branch Naming
1515

1616
```
17-
feature/<feature-name> # new feature
17+
feature/<feature-stem> # new feature
1818
fix/<issue-description> # bug fix
1919
refactor/<scope> # refactoring
2020
docs/<scope> # documentation
@@ -42,7 +42,7 @@ git commit -m "chore(deps): add python-dotenv dependency"
4242

4343
```bash
4444
# Push branch
45-
git push -u origin feature/<feature-name>
45+
git push -u origin feature/<feature-stem>
4646

4747
# Create PR
4848
gh pr create \

.opencode/skills/refactor/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ Refactoring commits are always **separate** from feature commits.
265265

266266
| Commit type | Message format | When |
267267
|---|---|---|
268-
| Preparatory refactoring | `refactor(<feature-name>): <what>` | Before RED, to make the feature easier |
269-
| REFACTOR phase | `refactor(<feature-name>): <what>` | After GREEN, cleaning up the green code |
270-
| Feature addition | `feat(<feature-name>): <what>` | After GREEN (never mixed with refactor) |
268+
| Preparatory refactoring | `refactor(<feature-stem>): <what>` | Before RED, to make the feature easier |
269+
| REFACTOR phase | `refactor(<feature-stem>): <what>` | After GREEN, cleaning up the green code |
270+
| Feature addition | `feat(<feature-stem>): <what>` | After GREEN (never mixed with refactor) |
271271

272272
Never mix a structural cleanup with a behavior addition in one commit. This keeps history bisectable and CI green at every commit.
273273

.opencode/skills/scope/SKILL.md

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Append all answered Q&A to `docs/discovery_journal.md`, in groups (general, cros
130130
Group headers use this format:
131131
- General group: `### General`
132132
- Cross-cutting group: `### <Group Name>`
133-
- Feature group: `### Feature: <feature-name>`
133+
- Feature group: `### Feature: <feature-stem>`
134134

135135
**Step B — Update .feature descriptions**
136136

@@ -216,7 +216,7 @@ Avoid: "As the system, I want..." (no business value). Break down stories that c
216216
- [ ] Rules collectively cover all entities in scope from the feature description
217217
- [ ] Every Rule passes the INVEST gate
218218

219-
Commit: `feat(stories): write user stories for <name>`
219+
Commit: `feat(stories): write user stories for <feature-stem>`
220220

221221
### Step B — Criteria
222222

@@ -244,7 +244,6 @@ All Rules must have their pre-mortems completed before any Examples are written.
244244
```
245245

246246
**Rules**:
247-
- `@id` tag on the line before `Example:`
248247
- `Example:` keyword (not `Scenario:`)
249248
- `Given/When/Then` in plain English
250249
- `Then` must be a single, observable, measurable outcome — no "and"
@@ -271,7 +270,6 @@ All Rules must have their pre-mortems completed before any Examples are written.
271270

272271
**Review checklist:**
273272
- [ ] Every `Rule:` block has at least one Example
274-
- [ ] Every `@id` is unique within this feature
275273
- [ ] Every Example has `Given/When/Then`
276274
- [ ] Every `Then` is a single, observable, measurable outcome
277275
- [ ] No Example tests implementation details
@@ -291,15 +289,14 @@ Communicate verbally to the next agent. Every `DISAGREE` is a **hard blocker**
291289
- No impl details: no Example tests internal state or implementation — AGREE/DISAGREE | file:line
292290
- Coverage: every entity in the feature description appears in at least one Rule — AGREE/DISAGREE | missing:
293291
- Distinct: no two Examples test the same observable behavior — AGREE/DISAGREE | file:line
294-
- Unique IDs: all @id values are unique within this feature — AGREE/DISAGREE
295292
- Pre-mortem: I ran a pre-mortem on each Rule and found no hidden failure modes — AGREE/DISAGREE | Rule:
296293
- Scope: no Example introduces behavior outside the feature boundary — AGREE/DISAGREE | file:line
297294

298-
Commit: `feat(criteria): write acceptance criteria for <name>`
295+
Commit: `feat(criteria): write acceptance criteria for <feature-stem>`
299296

300297
**After this commit, `Example:` blocks are frozen.** Any change requires:
301298
1. Add `@deprecated` tag to the old Example
302-
2. Write a new Example with a new `@id`
299+
2. Write a new Example (the `@id` tag will be assigned automatically)
303300

304301
---
305302

@@ -310,14 +307,14 @@ When a defect is reported against a completed or in-progress feature:
310307
1. **PO** adds a new Example to the relevant `Rule:` block in the `.feature` file:
311308

312309
```gherkin
313-
@bug @id:<new-8-char-hex>
310+
@bug
314311
Example: <what the bug is>
315312
Given <conditions that trigger the bug>
316313
When <action>
317314
Then <correct behavior>
318315
```
319316

320-
2. **SE** implements the specific test in `tests/features/<feature-name>/` (the `@id` test).
317+
2. **SE** implements the specific test in `tests/features/<feature_slug>/` (the `@id` test).
321318
3. **SE** also writes a `@given` Hypothesis property test in `tests/unit/` covering the whole class of inputs that triggered the bug — not just the single case.
322319
4. Both tests are required — neither is optional.
323320
5. SE follows the normal TDD loop (Step 3) for the new `@id`.
@@ -404,7 +401,7 @@ Status: IN-PROGRESS
404401
|----|----------|--------|
405402
| Q8 | ... | ... |
406403

407-
### Feature: <feature-name>
404+
### Feature: <feature-stem>
408405

409406
| ID | Question | Answer |
410407
|----|----------|--------|
@@ -435,7 +432,7 @@ success/failure conditions, and out-of-scope boundaries.>
435432
(First session only. Omit this subsection in subsequent sessions.)
436433

437434
### Feature List
438-
- `<feature-name>` — <one-sentence description of what changed or was added>
435+
- `<feature-stem>` — <one-sentence description of what changed or was added>
439436
(Write "No changes" if no features were added or modified this session.)
440437

441438
### Domain Model
@@ -459,12 +456,12 @@ Rules:
459456

460457
---
461458

462-
## YYYY-MM-DD — <feature-name>: <short title>
459+
## YYYY-MM-DD — <feature-stem>: <short title>
463460

464461
Decision: <what was decided — one sentence>
465462
Reason: <why — one sentence>
466463
Alternatives considered: <what was rejected and why>
467-
Feature: <feature-name>
464+
Feature: <feature-stem>
468465
```
469466

470467
Rules: Append-only. When a decision changes, append a new block that supersedes the old one. Cross-feature decisions use `Cross-feature:` in the header. Only write a block for non-obvious decisions with meaningful trade-offs.

.opencode/skills/session-workflow/SKILL.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Every session starts by reading state. Every session ends by writing state. This
2424
2. **If you are the PO** and Step 1 (SCOPE) is active: check `docs/discovery_journal.md` for the most recent session block.
2525
- If the most recent block has `Status: IN-PROGRESS` → the previous session was interrupted. Resume it before starting a new session: finish updating `.feature` files and `docs/discovery.md`, then mark the block `Status: COMPLETE`.
2626
3. If a feature is active at Step 2–5, read:
27-
- `docs/features/in-progress/<name>.feature` — feature file (Rules + Examples + @id)
27+
- `docs/features/in-progress/<feature-stem>.feature` — feature file (Rules + Examples + @id)
2828
- `docs/discovery.md` — project-level synthesis changelog (for context)
2929
4. Run `git status` — understand what is committed vs. what is not
3030
5. Confirm scope: you are working on exactly one step of one feature
@@ -43,7 +43,7 @@ Every session starts by reading state. Every session ends by writing state. This
4343
2. Commit any uncommitted work (even WIP):
4444
```bash
4545
git add -A
46-
git commit -m "WIP(<feature-name>): <what was done>"
46+
git commit -m "WIP(<feature-stem>): <what was done>"
4747
```
4848
3. If a step is fully complete, use the proper commit message instead of WIP.
4949

@@ -55,7 +55,7 @@ When a step completes within a session:
5555
2. Commit the TODO.md update:
5656
```bash
5757
git add TODO.md
58-
git commit -m "chore: complete step <N> for <feature-name>"
58+
git commit -m "chore: complete step <N> for <feature-stem>"
5959
```
6060
3. Only then begin the next step (in a new session where possible — see Rule 4).
6161

@@ -64,9 +64,9 @@ When a step completes within a session:
6464
```markdown
6565
# Current Work
6666

67-
Feature: <name>
67+
Feature: <feature-stem>
6868
Step: <1-5> (<step name>)
69-
Source: docs/features/in-progress/<name>.feature
69+
Source: docs/features/in-progress/<feature-stem>.feature
7070

7171
## Progress
7272
- [x] `@id:<hex>`: <description>
@@ -79,15 +79,15 @@ Run @<agent-name> — <one concrete action>
7979

8080
**"Next" line format**: Always prefix with `Run @<agent-name>` so the human knows exactly which agent to invoke. Agent names are defined in `AGENTS.md` — use the name exactly as listed there. Examples:
8181
- `Run @<software-engineer-agent> — implement @id:a1b2c3d4 (Step 3 RED)`
82-
- `Run @<software-engineer-agent> — load skill implementation and begin Step 2 (Architecture) for <feature-name>`
83-
- `Run @<reviewer-agent> — verify feature <feature-name> at Step 4`
82+
- `Run @<software-engineer-agent> — load skill implementation and begin Step 2 (Architecture) for <feature-stem>`
83+
- `Run @<reviewer-agent> — verify feature <feature-stem> at Step 4`
8484
- `Run @<product-owner-agent> — pick next BASELINED feature from backlog`
85-
- `Run @<product-owner-agent> — accept feature <feature-name> at Step 5`
85+
- `Run @<product-owner-agent> — accept feature <feature-stem> at Step 5`
8686

8787
**Source path by step:**
88-
- Step 1: `Source: docs/features/backlog/<name>.feature`
89-
- Steps 2–4: `Source: docs/features/in-progress/<name>.feature`
90-
- Step 5: `Source: docs/features/completed/<name>.feature`
88+
- Step 1: `Source: docs/features/backlog/<feature-stem>.feature`
89+
- Steps 2–4: `Source: docs/features/in-progress/<feature-stem>.feature`
90+
- Step 5: `Source: docs/features/completed/<feature-stem>.feature`
9191

9292
Status markers:
9393
- `[ ]` — not started
@@ -110,9 +110,9 @@ During Step 3 (TDD Loop), TODO.md **must** include a `## Cycle State` block to t
110110
```markdown
111111
# Current Work
112112

113-
Feature: <name>
113+
Feature: <feature-stem>
114114
Step: 3 (TDD Loop)
115-
Source: docs/features/in-progress/<name>.feature
115+
Source: docs/features/in-progress/<feature-stem>.feature
116116

117117
## Cycle State
118118
Test: `@id:<hex>` — <description>

0 commit comments

Comments
 (0)