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
package/guide/brownfield.md
CHANGED
|
@@ -9,6 +9,9 @@ npx kataster init --brownfield
|
|
|
9
9
|
/adopt # map the repository, then choose a slice
|
|
10
10
|
/adopt apps/billing # adopt one package
|
|
11
11
|
/adopt libs/shared-kernel # adopt a shared library
|
|
12
|
+
/adopt --cluster order # adopt a concept across api/, services/, repositories/
|
|
13
|
+
/adopt --table orders # start from a table and resolve the layers behind it
|
|
14
|
+
/adopt --route "POST /orders" # start from an entry point
|
|
12
15
|
```
|
|
13
16
|
|
|
14
17
|
## Adopt in slices, not in one pass
|
|
@@ -20,15 +23,21 @@ A monorepo with eighty packages cannot be understood in one session. `/adopt` th
|
|
|
20
23
|
2. **One slice** – one domain plus at most one shared library. Deep evidence, grilling, package docs, checks.
|
|
21
24
|
Then the next slice in a later session.
|
|
22
25
|
|
|
23
|
-
The map is generated from manifests and git history:
|
|
26
|
+
The map is generated from manifests, source files and git history:
|
|
24
27
|
|
|
25
|
-
- `docs/architecture/repo-map.json` – packages, paths, kinds, internal dependencies, consumers, adoption
|
|
28
|
+
- `docs/architecture/repo-map.json` – packages, paths, kinds, roles, internal dependencies, consumers, adoption
|
|
29
|
+
ranking, concept-slice suggestions
|
|
26
30
|
- `docs/architecture/repo-map.md` – the same as a readable map, plus dependency edges and a suggested first slice
|
|
27
31
|
- the **repository block in `AGENTS.md`** – tooling, layout, commands, adopted slices
|
|
28
32
|
- `.claude/project.json` → `repo.tooling` – Nx, Turborepo, Bazel, pnpm/npm/yarn workspaces, Go, Cargo, Gradle, Maven
|
|
29
33
|
|
|
30
34
|
The map is facts, not domain. It says where code lives, never what a word means.
|
|
31
35
|
|
|
36
|
+
In a layered repository (`api/`, `services/`, `repositories/`, `utils/`) a package is not a domain. The map marks
|
|
37
|
+
generic packages as **layer packages** and clusters files by shared concept stems and tables. A domain such as
|
|
38
|
+
“order” shows up as an `order` slice across all layers; `slice.ts` resolves the candidate files from a cluster, a
|
|
39
|
+
table, a route, an event ID, a glob or a term. The resolver is evidence – you confirm the scope.
|
|
40
|
+
|
|
32
41
|
## What `/adopt` does
|
|
33
42
|
|
|
34
43
|
### 1. Chooses the slice with you
|
|
@@ -56,18 +65,26 @@ A monorepo gets three lenses:
|
|
|
56
65
|
A small repository without monorepo tooling gets the five classic lenses: structure and stack, domain language,
|
|
57
66
|
data and integrations, behaviour and tests, history and decisions.
|
|
58
67
|
|
|
68
|
+
For a concept slice, the analysts receive the confirmed file list from `slice.ts`, not only a package path. They
|
|
69
|
+
report facts inside that scope; widening the slice is the user's decision, not theirs.
|
|
70
|
+
|
|
59
71
|
Each analyst returns facts with `path:line`, candidate vocabulary, candidate events/commands/read models,
|
|
60
72
|
contradictions, and questions with a recommended answer. Confidence is `high`, `medium` or `low`.
|
|
61
73
|
|
|
62
74
|
### 4. Drafts the slice
|
|
63
75
|
|
|
64
76
|
- `CONTEXT.md` – the slice's terms, competing spellings under `_Avoid_`
|
|
65
|
-
- `docs/domain/events.yaml` – 10–25 candidate events, commands, actors, read models, flows, hotspots
|
|
66
|
-
|
|
77
|
+
- `docs/domain/events.yaml` – 10–25 candidate events, commands, actors, read models, flows, hotspots. Each
|
|
78
|
+
aggregate is `as-is` with evidence (table, service method, path) or `unresolved` with an open hotspot – never
|
|
79
|
+
an invented `target`
|
|
80
|
+
- `docs/domain/context-map.as-is.md` (recovered boundaries, with evidence) and
|
|
81
|
+
`docs/architecture/data-model.md` (as-is). The target map `docs/domain/context-map.md` is read, never written
|
|
82
|
+
here; if it does not exist yet, the gap register says “target not decided”.
|
|
67
83
|
- `docs/engineering/conventions.md` – how the code actually works today, including the monorepo tooling
|
|
68
84
|
- `docs/adr/` – de-facto decisions worth recording (`status: proposed`)
|
|
69
|
-
- **
|
|
70
|
-
`
|
|
85
|
+
- **domain docs, by scope** – a real domain package gets `<package>/AGENTS.md` plus `<package>/CLAUDE.md` from
|
|
86
|
+
`package-doc.ts <path>`; a cross-cutting concept slice gets `.claude/rules/<context>.md` with `paths:` from
|
|
87
|
+
`slice-doc.ts <slug> --context BC-… --paths "…"`. One mechanism per scope, never both.
|
|
71
88
|
|
|
72
89
|
Everything uncertain is marked. The draft is a proposal, not a verdict.
|
|
73
90
|
|
|
@@ -92,19 +109,25 @@ baseline of existing violations; only new violations fail. A hook that is red on
|
|
|
92
109
|
`/adopt` may add characterization tests and tooling. It never changes production code, and it never writes a test
|
|
93
110
|
that fails on existing behaviour – that would be a story.
|
|
94
111
|
|
|
112
|
+
The same net carries into `/implement`: `characterize.ts --story ST-NNN` names the affected source files and the
|
|
113
|
+
tests it can find. A path with no test for the behaviour that is about to change gets a characterization test
|
|
114
|
+
first – current behaviour pinned, green on unchanged code, committed separately, then the story's red → green loop.
|
|
115
|
+
If the current behaviour contradicts the story, the story goes back to `review`; the test is not bent to fit.
|
|
116
|
+
|
|
95
117
|
### 7. Records the gap – stories only on request
|
|
96
118
|
|
|
97
119
|
| Class | Default |
|
|
98
120
|
|---|---|
|
|
99
|
-
| Exists and is intended | Documentation (data model, context map) – no action |
|
|
121
|
+
| Exists and is intended | Documentation (data model, as-is context map) – no action |
|
|
100
122
|
| Exists but contradicts the model | Candidate in the gap register |
|
|
101
123
|
| Missing | Candidate in the gap register |
|
|
102
124
|
| Dead | Candidate: removal or an explicit "leave it" |
|
|
103
125
|
|
|
104
|
-
**The first adoption pass creates no stories.** It records the gap in `docs/domain/adoption-report.md`
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`
|
|
126
|
+
**The first adoption pass creates no stories.** It records the gap in `docs/domain/adoption-report.md` and
|
|
127
|
+
`docs/domain/gap-register.md` (one generated row per `as-is`/`unresolved` aggregate, with the as-is and target
|
|
128
|
+
context links), and open questions in `OPEN_QUESTIONS.md`, so the model can be reviewed before anyone commits to
|
|
129
|
+
work. Only `/adopt <scope> --stories` (or an explicit "turn these into stories") turns the selected gaps into
|
|
130
|
+
stories in the main session (spec engineer, spec linter gate).
|
|
108
131
|
|
|
109
132
|
**Behaviour that already works does not become a story.** That turns the backlog into a description of the past.
|
|
110
133
|
|
|
@@ -114,11 +137,14 @@ questions in `OPEN_QUESTIONS.md`), so the model can be reviewed before anyone co
|
|
|
114
137
|
and checks, gaps, what was deliberately left alone, open hotspots, next slice.
|
|
115
138
|
|
|
116
139
|
```bash
|
|
140
|
+
# a real domain package
|
|
117
141
|
node .claude/skills/adopt/scripts/repo-map.ts --adopt apps/billing
|
|
142
|
+
# a concept slice across layers
|
|
143
|
+
node .claude/skills/adopt/scripts/repo-map.ts --adopt-slice order
|
|
118
144
|
```
|
|
119
145
|
|
|
120
146
|
The next session sees the progress in `AGENTS.md` and the map, and starts the next slice with
|
|
121
|
-
`/adopt <
|
|
147
|
+
`/adopt <scope>`.
|
|
122
148
|
|
|
123
149
|
## What lives where
|
|
124
150
|
|
|
@@ -134,6 +160,9 @@ The next session sees the progress in `AGENTS.md` and the map, and starts the ne
|
|
|
134
160
|
| `docs/engineering/checks.yaml` | on check | what runs at edit time and at Stop/CI |
|
|
135
161
|
| `docs/architecture/repo-map.md` | on demand | generated map: packages, edges, consumers, ranking |
|
|
136
162
|
| `CONTEXT.md` | every session | the domain language |
|
|
163
|
+
| `docs/domain/context-map.md` | on demand | **target** contexts and relationships |
|
|
164
|
+
| `docs/domain/context-map.as-is.md` | on demand | **as-is** snapshot recovered by `/adopt`, with evidence |
|
|
165
|
+
| `docs/domain/gap-register.md` | on demand | generated as-is ↔ target rows and the user's decisions |
|
|
137
166
|
|
|
138
167
|
Nested `AGENTS.md` is loaded on demand by OpenCode; Claude Code loads the nested `CLAUDE.md` when it reads or
|
|
139
168
|
edits a file there. That is why every package gets both files.
|
package/guide/concepts.md
CHANGED
|
@@ -14,6 +14,14 @@ Two rules make it work:
|
|
|
14
14
|
user something you could find yourself.
|
|
15
15
|
- **Do not act until the frontier is empty** and the user confirms a shared understanding.
|
|
16
16
|
|
|
17
|
+
When a frontier question rests on an **external fact** – a standard, a library capability, a regulatory requirement,
|
|
18
|
+
a security or privacy rule – the agent researches it before the round. The `researcher` is read-only and returns
|
|
19
|
+
findings with source, tier, retrieval date and confidence; the main session writes them to
|
|
20
|
+
`docs/research/<topic>.md` with an expiry. The finding is evidence, not a decision: the recommendation cites it
|
|
21
|
+
(`➡️ … (RFC 9700 §2.1.1; F1 in docs/research/oauth-pkce.md)`), and only a confirmed finding becomes a testable
|
|
22
|
+
criterion. A trigger is not a mandate – if the model, an ADR or a valid research file already answers it, nothing
|
|
23
|
+
runs.
|
|
24
|
+
|
|
17
25
|
`/grill-me` is the pure form. `/grill-with-docs` adds the glossary and ADRs. Every discovery command embeds it.
|
|
18
26
|
|
|
19
27
|
## The artifacts
|
|
@@ -22,13 +30,16 @@ Two rules make it work:
|
|
|
22
30
|
|---|---|---|
|
|
23
31
|
| `CONTEXT.md` | Glossary – the ubiquitous language. One canonical word per concept, alternatives under `_Avoid_` | user, maintained by the agent |
|
|
24
32
|
| `docs/domain/events.yaml` | Event storming model: actors, contexts, aggregates, events, commands, policies, read models, hotspots, flows | user, written by the agent |
|
|
25
|
-
| `docs/domain/context-map.md` |
|
|
33
|
+
| `docs/domain/context-map.md` | **Target** bounded contexts and their relationships (DDD patterns) | domain-architect, `/bootstrap`, `/shape`, `/feature`, `/improve-codebase-architecture` |
|
|
34
|
+
| `docs/domain/context-map.as-is.md` | **As-is** snapshot recovered from code, with evidence | `/adopt` only |
|
|
35
|
+
| `docs/domain/gap-register.md` | Generated rows for every `as-is`/`unresolved` aggregate, linking as-is and target; decisions outside the block | `/adopt`, user |
|
|
26
36
|
| `docs/architecture/data-model.md` | Logical, stack-neutral model per aggregate; lifecycle and ER diagrams | domain-architect |
|
|
27
37
|
| `docs/engineering/conventions.md` | How code is built here, including the seam catalog | user |
|
|
28
38
|
| `docs/adr/` | Architecture decisions. `proposed` until the user accepts; accepted ones are superseded, never edited | user |
|
|
29
|
-
| `docs/stories/` | The backlog. One Markdown file per story; `BACKLOG.md` is generated. With a tracker, this is the local mirror |
|
|
39
|
+
| `docs/stories/` | The backlog. One Markdown file per story; `BACKLOG.md` is generated. With a tracker, this is the local mirror | main session as spec engineer; spec linter; `/backlog-sync` with a tracker |
|
|
30
40
|
| `docs/product/vision.md` | Problem, user groups, goals, non-goals, success criteria | user |
|
|
31
41
|
| `docs/product/ux-guidelines.md` | The interface yardstick, as numbered rules with date and reason | ux-designer, approved by user |
|
|
42
|
+
| `docs/research/<topic>.md` | Evidence for a cross-cutting question: findings with source, tier, confidence and an expiry. Evidence, not a specification | researcher (read-only); main session writes; user confirms findings |
|
|
32
43
|
| `docs/prototypes/<slug>/` | Throwaway prototypes on their own branch; only the README stays on the default branch | ux-designer; verdict in the story and the guidelines |
|
|
33
44
|
|
|
34
45
|
## Events, commands, aggregates
|
|
@@ -46,15 +57,21 @@ The code carries the same IDs, which is what makes traceability checkable.
|
|
|
46
57
|
|
|
47
58
|
## Stories
|
|
48
59
|
|
|
49
|
-
A story is a vertical slice of observable behaviour: `draft → review → ready →
|
|
50
|
-
`superseded`).
|
|
60
|
+
A story is a vertical slice of observable behaviour: `draft → review → ready-for-human-review → ready →
|
|
61
|
+
in-progress → done` (plus `superseded`).
|
|
51
62
|
|
|
63
|
+
- **The linter is the gate.** `lint-story.ts` checks the spec deterministically. Green → `ready-for-human-review`;
|
|
64
|
+
the user approves `ready`.
|
|
52
65
|
- **Discovery ends at `ready`.** Engineering moves `ready → in-progress → done`.
|
|
66
|
+
- **1-command rule.** One domain story = one command + its events on one aggregate, at most 4 scenarios. A second
|
|
67
|
+
command is a second story.
|
|
68
|
+
- **Fast lane.** `type: chore`/`task` for UI tweaks, refactors, dependencies and setup – no Gherkin matching, no
|
|
69
|
+
formal reviews.
|
|
53
70
|
- **A story that turns out wrong goes back to `review`** with an `[OPEN]` and a row in `OPEN_QUESTIONS.md` – never
|
|
54
71
|
guessed into shape during implementation.
|
|
55
72
|
- **`done` is final.** A change to finished behaviour is a new story.
|
|
56
|
-
- **
|
|
57
|
-
|
|
73
|
+
- **A scenario title becomes a test title**: `ST-NNN: <scenario title>`. `verify.ts` checks the link for domain
|
|
74
|
+
stories.
|
|
58
75
|
|
|
59
76
|
### Stories from tickets
|
|
60
77
|
|
package/guide/greenfield.md
CHANGED
|
@@ -62,12 +62,13 @@ Two kinds of work, kept apart:
|
|
|
62
62
|
- **The first vertical slice** – the smallest set of stories that delivers one real end-to-end path from the
|
|
63
63
|
vision, each with Gherkin scenarios.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
One story = one command on one aggregate, at most 4 scenarios; no implementation plan inside a domain story; every domain story links `aggregate`, `command` and its `events`.
|
|
66
66
|
|
|
67
67
|
## Phase 6 – Review
|
|
68
68
|
|
|
69
|
-
`/review-stories` –
|
|
70
|
-
pull before the review and push the
|
|
69
|
+
`/review-stories` – the spec linter checks frontmatter, aggregate/command/events, Gherkin and the scenario limit.
|
|
70
|
+
Green → `ready-for-human-review`; you approve every `ready`. With a tracker, pull before the review and push the
|
|
71
|
+
approved stories afterwards (`/backlog-sync`).
|
|
71
72
|
|
|
72
73
|
## Phase 7 – Build
|
|
73
74
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
6
|
import { Installer, VERSION } from "./install.mjs";
|
|
@@ -12,6 +12,8 @@ const HELP = `Kataster ${VERSION} – agent workflow for domain-driven discovery
|
|
|
12
12
|
Usage:
|
|
13
13
|
kataster init [dir] Install the workflow into a repository (default: .)
|
|
14
14
|
kataster upgrade [dir] Update the workflow, keep project content and local changes
|
|
15
|
+
kataster lint-story [ST-NNN|file]
|
|
16
|
+
Deterministic spec linter for stories (no LLM); empty = all stories
|
|
15
17
|
kataster doctor [dir] Check the installation
|
|
16
18
|
kataster --version
|
|
17
19
|
kataster --help
|
|
@@ -99,8 +101,8 @@ Useful commands afterwards:
|
|
|
99
101
|
/grill-me /grill-with-docs sharpen a plan and write the docs as you go
|
|
100
102
|
/event-storming explore a process or the whole domain
|
|
101
103
|
/feature <idea> add a feature through grilling, storming, stories
|
|
102
|
-
/review-stories
|
|
103
|
-
/implement [ST-NNN] test-first implementation of one story
|
|
104
|
+
/review-stories linter gate + human approval before a story is ready
|
|
105
|
+
/implement [ST-NNN] test-first implementation of one story (--fast for chore/task)
|
|
104
106
|
/backlog-sync pull/push the backlog mirror (Jira or GitHub Issues)
|
|
105
107
|
/retro retrospective on a session
|
|
106
108
|
`);
|
|
@@ -219,6 +221,36 @@ async function upgrade(options) {
|
|
|
219
221
|
console.log("\nUpgrade complete. Project content (CONTEXT.md, docs/domain, docs/stories, ADRs) was not touched.");
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
/** Nearest directory that has the story linter installed (the repository of the given file, or an ancestor). */
|
|
225
|
+
function findInstallRoot(start) {
|
|
226
|
+
let dir = resolve(start);
|
|
227
|
+
if (dir.endsWith(".md")) dir = dirname(dir);
|
|
228
|
+
for (;;) {
|
|
229
|
+
if (existsSync(join(dir, ".claude", "skills", "user-stories", "scripts", "lint-story.ts"))) return dir;
|
|
230
|
+
const parent = dirname(dir);
|
|
231
|
+
if (parent === dir) return null;
|
|
232
|
+
dir = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* `kataster lint-story [ST-NNN|file]` – delegates to the installed spec linter. Deterministic: the same checks
|
|
238
|
+
* the hook, `/review-stories` and `verify.ts` run, without an LLM.
|
|
239
|
+
*/
|
|
240
|
+
function lintStory(options) {
|
|
241
|
+
const arg = options._[1];
|
|
242
|
+
const root = findInstallRoot(arg && arg.endsWith(".md") ? arg : (arg && /^ST-/i.test(arg) ? process.cwd() : (arg || process.cwd())));
|
|
243
|
+
if (!root) {
|
|
244
|
+
throw new Error("No Kataster installation found (missing .claude/skills/user-stories/scripts/lint-story.ts) – run `kataster init` first.");
|
|
245
|
+
}
|
|
246
|
+
const script = join(root, ".claude", "skills", "user-stories", "scripts", "lint-story.ts");
|
|
247
|
+
const args = [script];
|
|
248
|
+
if (arg) args.push(arg.endsWith(".md") ? resolve(arg) : arg);
|
|
249
|
+
if (options.json) args.push("--json");
|
|
250
|
+
const r = spawnSync(process.execPath, args, { cwd: root, stdio: "inherit" });
|
|
251
|
+
process.exitCode = r.status ?? 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
222
254
|
function doctor(options) {
|
|
223
255
|
const root = resolve(options._[1] ?? ".");
|
|
224
256
|
const checks = [];
|
|
@@ -230,6 +262,11 @@ function doctor(options) {
|
|
|
230
262
|
add("Install manifest", Boolean(manifest), manifest ? `version ${manifest.version}` : "missing – run init");
|
|
231
263
|
const leftovers = existsSync(join(root, ".laguardia")) || existsSync(join(root, ".opencode", "plugins", "laguardia"));
|
|
232
264
|
add("No leftovers from the old name", !leftovers, leftovers ? "run upgrade to migrate them" : "");
|
|
265
|
+
const retired = [
|
|
266
|
+
".claude/agents/product-owner.md", ".claude/agents/requirements-engineer.md", ".claude/agents/lead-dev.md",
|
|
267
|
+
".opencode/agents/product-owner.md", ".opencode/agents/requirements-engineer.md", ".opencode/agents/lead-dev.md",
|
|
268
|
+
].filter((file) => existsSync(join(root, file)));
|
|
269
|
+
add("No retired role-play agents", retired.length === 0, retired.join(", ") || "clean");
|
|
233
270
|
const profile = readJson(join(root, ".claude", "project.json"));
|
|
234
271
|
add("Project profile", Boolean(profile?.name), profile?.name ?? "missing");
|
|
235
272
|
|
|
@@ -238,7 +275,7 @@ function doctor(options) {
|
|
|
238
275
|
add("Backlog provider", ["local", "github", "jira"].includes(provider), provider);
|
|
239
276
|
if (provider === "jira") {
|
|
240
277
|
add("Backlog: Jira project", Boolean(backlog.jira?.project), backlog.jira?.project || "backlog.jira.project is missing");
|
|
241
|
-
const statuses = ["draft", "review", "ready", "in-progress", "done", "superseded"];
|
|
278
|
+
const statuses = ["draft", "review", "ready-for-human-review", "ready", "in-progress", "done", "superseded"];
|
|
242
279
|
const missingStatuses = statuses.filter((status) => !backlog.jira?.statusMap?.[status]);
|
|
243
280
|
add("Backlog: Jira status map", missingStatuses.length === 0, missingStatuses.join(", ") || "all statuses mapped");
|
|
244
281
|
const priorities = ["must", "should", "could", "wont"];
|
|
@@ -296,6 +333,7 @@ export async function main(argv) {
|
|
|
296
333
|
const command = options._[0];
|
|
297
334
|
if (command === "init") return init(options);
|
|
298
335
|
if (command === "upgrade") return upgrade(options);
|
|
336
|
+
if (command === "lint-story") return lintStory(options);
|
|
299
337
|
if (command === "doctor") return doctor(options);
|
|
300
338
|
throw new Error(`Unknown command '${command}'. Try --help.`);
|
|
301
339
|
}
|
package/src/detect.mjs
CHANGED
|
@@ -224,6 +224,15 @@ export function buildProfile(root, { name, kind, uiLanguage, artifactLanguage, d
|
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
226
|
},
|
|
227
|
+
// Research files expire. The gate asks for `sources:` on risky work before it reaches ready-for-human-review.
|
|
228
|
+
// Keep the defaults in sync with .claude/lib/config.ts (DEFAULT_RESEARCH_CONFIG).
|
|
229
|
+
research: {
|
|
230
|
+
expiryMonths: 12,
|
|
231
|
+
gate: {
|
|
232
|
+
risk: ["high"],
|
|
233
|
+
labels: ["security", "auth", "payment", "privacy"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
227
236
|
setup: {
|
|
228
237
|
envFile: "",
|
|
229
238
|
envHint: "",
|
package/src/install.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import {
|
|
@@ -58,8 +58,8 @@ const HOOK_EVENTS = {
|
|
|
58
58
|
const ALLOW = [
|
|
59
59
|
"Bash(node .claude/skills/event-storming/scripts/validate-events.ts)",
|
|
60
60
|
"Bash(node .claude/skills/event-storming/scripts/render-event-storming.ts)",
|
|
61
|
-
"Bash(node .claude/skills/user-stories/scripts
|
|
62
|
-
"Bash(node .claude/skills/
|
|
61
|
+
"Bash(node .claude/skills/user-stories/scripts/*)",
|
|
62
|
+
"Bash(node .claude/skills/research/scripts/*)",
|
|
63
63
|
"Bash(node .claude/skills/adopt/scripts/*)",
|
|
64
64
|
"Bash(node .claude/skills/backlog/scripts/*)",
|
|
65
65
|
"Bash(node .claude/skills/implement/scripts/*)",
|
|
@@ -70,6 +70,21 @@ const ALLOW = [
|
|
|
70
70
|
"Bash(git log *)",
|
|
71
71
|
];
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Files earlier versions shipped and 0.4 removed. `upgrade` deletes them from an existing installation – but only
|
|
75
|
+
* when the manifest tracks them and the local copy is unmodified, so a project that customised one keeps it.
|
|
76
|
+
* Add a path here whenever a workflow file is retired; never prune by "not in the payload", because files from
|
|
77
|
+
* optional extras and user-added agents are not the installer's to delete.
|
|
78
|
+
*/
|
|
79
|
+
const OBSOLETE = [
|
|
80
|
+
".claude/agents/product-owner.md",
|
|
81
|
+
".claude/agents/requirements-engineer.md",
|
|
82
|
+
".claude/agents/lead-dev.md",
|
|
83
|
+
".opencode/agents/product-owner.md",
|
|
84
|
+
".opencode/agents/requirements-engineer.md",
|
|
85
|
+
".opencode/agents/lead-dev.md",
|
|
86
|
+
];
|
|
87
|
+
|
|
73
88
|
export class Installer {
|
|
74
89
|
constructor(root, { dryRun = false, force = false, log = () => {} } = {}) {
|
|
75
90
|
this.root = resolve(root);
|
|
@@ -165,17 +180,52 @@ export class Installer {
|
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
copyWorkflowDirs() {
|
|
183
|
+
const present = new Set();
|
|
168
184
|
for (const dir of WORKFLOW_DIRS) {
|
|
169
185
|
const source = join(PKG, dir);
|
|
170
186
|
if (!existsSync(source)) continue;
|
|
171
187
|
for (const file of walk(source)) {
|
|
172
188
|
const name = toPosix(relative(source, file));
|
|
173
189
|
if (name.split("/").some((part) => SKIP.has(part))) continue;
|
|
174
|
-
const
|
|
190
|
+
const key = relPosix(this.root, join(this.root, dir, name));
|
|
191
|
+
present.add(key);
|
|
175
192
|
// settings.json and opencode.jsonc are merged, not replaced
|
|
176
193
|
if (file === join(source, "settings.json") || file === join(source, "opencode.jsonc")) continue;
|
|
177
|
-
this.put(join(this.root, dir, name),
|
|
194
|
+
this.put(join(this.root, dir, name), read(file));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.pruneObsolete(present);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Removes the retired workflow files listed in OBSOLETE (e.g. the deleted corporate-roleplay agents).
|
|
202
|
+
*
|
|
203
|
+
* Only files recorded in the manifest are candidates, so project content and files from other tools are never
|
|
204
|
+
* touched. A file the user edited is kept and reported as a conflict – unless `--force` is given, which backs it
|
|
205
|
+
* up first. This is what makes a removed agent actually disappear from an existing installation on upgrade.
|
|
206
|
+
*/
|
|
207
|
+
pruneObsolete(present) {
|
|
208
|
+
for (const key of OBSOLETE) {
|
|
209
|
+
if (present.has(key)) continue;
|
|
210
|
+
if (!(key in (this.manifest?.files ?? {}))) continue;
|
|
211
|
+
|
|
212
|
+
const path = join(this.root, key);
|
|
213
|
+
if (!existsSync(path)) {
|
|
214
|
+
delete this.files[key];
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const known = this.manifest.files[key];
|
|
219
|
+
const local = sha(read(path));
|
|
220
|
+
if (local !== known && !this.force) {
|
|
221
|
+
this.conflicts.push(key);
|
|
222
|
+
this.record("conflict", path, "retired, but locally modified – kept");
|
|
223
|
+
continue;
|
|
178
224
|
}
|
|
225
|
+
if (this.force) this.backup(path);
|
|
226
|
+
if (!this.dryRun) rmSync(path, { force: true });
|
|
227
|
+
delete this.files[key];
|
|
228
|
+
this.record("remove", path, "retired workflow file");
|
|
179
229
|
}
|
|
180
230
|
}
|
|
181
231
|
|
|
@@ -384,6 +434,9 @@ This repository runs the Kataster workflow. Discovery lives in \`.claude/skills/
|
|
|
384
434
|
local mirror and the tracker is the source of truth – sync with \`/backlog-sync\`; \`BACKLOG.md\` stays generated.
|
|
385
435
|
- **Map:** \`docs/workflow.md\` – sources of truth, commands, phases, rules. Read it when unsure where something
|
|
386
436
|
belongs.
|
|
437
|
+
- **Research:** cross-cutting questions go through the read-only \`researcher\` (\`/research\`,
|
|
438
|
+
\`/feature --research\`); findings live in \`docs/research/<topic>.md\` with an expiry and are cited from a story's
|
|
439
|
+
\`sources:\`. Evidence, not a specification.
|
|
387
440
|
- **Repo map (brownfield):** \`docs/architecture/repo-map.md\` – packages, dependency edges, consumers, adopted
|
|
388
441
|
slices. A package's \`AGENTS.md\` is loaded only when you work there; the root file stays free of domain.
|
|
389
442
|
- **Start:** greenfield → \`/bootstrap\`; existing code → \`/adopt\` (in slices, not the whole monorepo).
|
|
@@ -391,9 +444,16 @@ This repository runs the Kataster workflow. Discovery lives in \`.claude/skills/
|
|
|
391
444
|
- **Project checks:** \`docs/engineering/checks.yaml\` – what runs at edit time and Stop/CI; architecture boundaries
|
|
392
445
|
in \`docs/architecture/boundaries.yaml\` with existing debt in the baseline (only new violations fail).
|
|
393
446
|
- **Language:** domain terms exactly as in \`CONTEXT.md\`; UI words from its \`_UI (<lang>)_\` lines.
|
|
394
|
-
- **
|
|
447
|
+
- **1-command rule:** one story = one command + its events on one aggregate, at most 4 Gherkin scenarios. Full
|
|
448
|
+
domain stories are \`type: domain\`; UI tweaks, refactors, dependencies and setup use the fast lane
|
|
449
|
+
(\`type: chore\` or \`type: task\`).
|
|
450
|
+
- **Spec gate:** \`node .claude/skills/user-stories/scripts/lint-story.ts <file>\` must be green. A story the linter
|
|
451
|
+
accepts becomes \`ready-for-human-review\`; only the user approves \`ready\`.
|
|
452
|
+
- **Traceability:** every domain scenario has a test titled \`ST-NNN: <scenario title>\`; domain IDs come from
|
|
395
453
|
\`docs/domain/events.yaml\`.
|
|
396
454
|
- **ADRs:** new ones are \`proposed\`; only the user sets \`accepted\`. A \`done\` story is never reopened.
|
|
455
|
+
- **Reviews:** \`/implement\` scales review to the diff. \`code-reviewer\` and \`acceptance-tester\` report findings and
|
|
456
|
+
never create stories; fast-lane stories skip the formal review pass.
|
|
397
457
|
- **Gate:** \`node .claude/skills/implement/scripts/verify.ts\`.
|
|
398
458
|
|
|
399
459
|
Keep this block short: it is loaded into every session. Put rules into the skill that owns them, not here.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Target context map – {{project}}
|
|
2
|
+
|
|
3
|
+
> Where the domain is going, not what the code does today. The as-is map is `context-map.as-is.md` and is
|
|
4
|
+
> written only by `/adopt`; the gap register links both sides. `/bootstrap`, `/shape`, `/feature`,
|
|
5
|
+
> `/event-storming`, `domain-architect` and `/improve-codebase-architecture` write this file.
|
|
2
6
|
|
|
3
7
|
<!--
|
|
4
8
|
One section per bounded context: purpose, core terms, owner role. Relationships as a Mermaid flowchart
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Written by /bootstrap, /adopt, /event-storming and /feature. Validated and rendered to
|
|
8
8
|
# docs/domain/event-storming.md after every write. An empty model is valid: it grows during discovery.
|
|
9
|
+
#
|
|
10
|
+
# Aggregates carry an optional status: target (default when missing), as-is (factual boundary, needs evidence),
|
|
11
|
+
# or unresolved (boundary unclear, needs an open hotspot and grilling; no story on it can be ready).
|
|
9
12
|
|
|
10
13
|
version: 1
|
|
11
14
|
domain: {{project}}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Research
|
|
2
|
+
|
|
3
|
+
Evidence for cross-cutting questions: authentication and identity, security, payment, privacy and data protection,
|
|
4
|
+
accessibility, performance, or a new library or technology. The files here are **evidence, not a specification** –
|
|
5
|
+
the user decides in grilling, and only a confirmed finding becomes a testable acceptance criterion.
|
|
6
|
+
|
|
7
|
+
One file per topic: `docs/research/<topic>.md`, lowercase kebab-case, no date in the file name. Refresh in place;
|
|
8
|
+
if the question changes materially, use a new topic.
|
|
9
|
+
|
|
10
|
+
## Format
|
|
11
|
+
|
|
12
|
+
```markdown
|
|
13
|
+
---
|
|
14
|
+
topic: oauth-pkce-spa
|
|
15
|
+
question: "Which PKCE configuration is required for a public SPA client?"
|
|
16
|
+
created: 2026-11-07
|
|
17
|
+
retrieved: 2026-11-07
|
|
18
|
+
expires: 2027-11-07
|
|
19
|
+
stack: ["next@15.2.0", "node@22.18.0"]
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Research: …
|
|
23
|
+
## Question
|
|
24
|
+
## Findings
|
|
25
|
+
### F1 – …
|
|
26
|
+
- **Statement:** …
|
|
27
|
+
- **Source:** <title> – <url> (<section>)
|
|
28
|
+
- **Retrieved:** YYYY-MM-DD
|
|
29
|
+
- **Tier:** 1 | 2 | 3
|
|
30
|
+
- **Confidence:** high | medium | low
|
|
31
|
+
- **Domain-model impact:** none | conflicts with `<term>` / `AGG-…` / ADR-… – what
|
|
32
|
+
## Conflicts, caveats and gaps
|
|
33
|
+
## Open questions for the grilling
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Source tiers: **1** specs, standards and authorities (RFC, W3C/WAI, OWASP, NIST, ISO, GDPR/EDPB, PCI SSC, vendor
|
|
37
|
+
security advisories); **2** official documentation or source of the exact version in the lockfile; **3** everything
|
|
38
|
+
else – a hint, never the sole basis for a criterion.
|
|
39
|
+
|
|
40
|
+
## Expiry
|
|
41
|
+
|
|
42
|
+
`expires` is computed when the file is written or refreshed: `retrieved + research.expiryMonths` from
|
|
43
|
+
`.claude/project.json` (default 12 months, month-end clamped). It is not recomputed on every read. A stale file is
|
|
44
|
+
refreshed, not silently reused.
|
|
45
|
+
|
|
46
|
+
Check a file, every referenced file, or the whole directory:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node .claude/skills/research/scripts/research-check.ts --topic <slug>
|
|
50
|
+
node .claude/skills/research/scripts/research-check.ts --referenced
|
|
51
|
+
node .claude/skills/research/scripts/research-check.ts --all
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Statuses are `VALID`, `EXPIRED`, `MISSING` and `INVALID`; exit 1 means at least one file needs a refresh or repair.
|
|
55
|
+
`verify.ts` runs the referenced check as an advisory step – an expired file warns, it never fails the gate.
|
|
56
|
+
|
|
57
|
+
## How research enters the work
|
|
58
|
+
|
|
59
|
+
- `/research <question>` runs the read-only `researcher`; the main session writes its findings verbatim into this
|
|
60
|
+
directory. `/feature --research` forces the step for a topic the trigger list does not catch.
|
|
61
|
+
- The story lists the file under `sources:`. A story with `risk: high` or one of the configured gate labels in
|
|
62
|
+
`.claude/project.json` (default `security`, `auth`, `payment`, `privacy`) needs at least one resolvable source
|
|
63
|
+
before `ready-for-human-review`; the linter warns, `story-status.ts` blocks.
|
|
64
|
+
- Adopted findings become testable criteria with an observable threshold. Rejected options are recorded with the
|
|
65
|
+
reason in the story, ADR or initiative – this file stays evidence, not a decision log.
|
|
66
|
+
- A finding that contradicts `CONTEXT.md`, `events.yaml`, an accepted ADR or the UX guidelines is a grilling
|
|
67
|
+
question, never a silent rewrite of the model.
|
|
68
|
+
|
|
69
|
+
The full workflow is `.claude/skills/research/SKILL.md`; the format template is
|
|
70
|
+
`.claude/skills/research/templates/research.md`.
|
|
@@ -4,9 +4,18 @@ One Markdown file per story: `ST-NNN-short-title.md`. `BACKLOG.md` is generated
|
|
|
4
4
|
Rules, format and splitting: `.claude/skills/user-stories/SKILL.md`.
|
|
5
5
|
Template: `.claude/skills/user-stories/templates/story-template.md`.
|
|
6
6
|
|
|
7
|
-
Workflow: `draft → review → ready → in-progress → done` (plus `superseded`).
|
|
8
|
-
|
|
9
|
-
-
|
|
7
|
+
Workflow: `draft → review → ready-for-human-review → ready → in-progress → done` (plus `superseded`).
|
|
8
|
+
|
|
9
|
+
- The **spec linter** is the gate: `node .claude/skills/user-stories/scripts/lint-story.ts <file>` (or
|
|
10
|
+
`kataster lint-story`). It checks frontmatter, `aggregate`/`command`/events against `docs/domain/events.yaml`,
|
|
11
|
+
Gherkin `Given/When/Then` and the scenario limit – deterministically, no LLM.
|
|
12
|
+
- Green → `ready-for-human-review`; **the user approves `ready`**. `/implement` only picks up `ready`.
|
|
13
|
+
- **1-command rule:** one `type: domain` story = one command + its events on one aggregate, at most 4 scenarios.
|
|
14
|
+
- An aggregate with `status: unresolved` blocks the story at `ready-for-human-review`; settle the boundary and
|
|
15
|
+
its hotspot in grilling first. A missing status means `target`; `as-is` is allowed.
|
|
16
|
+
- **Fast lane:** `type: chore` or `type: task` for UI tweaks, refactorings, dependencies and setup – no Gherkin
|
|
17
|
+
matching, no formal reviews. `type: tech-task` keeps its checklist; `type: spike` is a timeboxed question.
|
|
18
|
+
- `ui: true` marks a story with a frontend part; it triggers the on-demand UX utility, never a gate.
|
|
10
19
|
- A story that turns out wrong goes back to `review` with an `[OPEN]` and a row in `OPEN_QUESTIONS.md`.
|
|
11
20
|
- `done` is final: a change to finished behaviour is a new story.
|
|
12
21
|
|