kataster 0.3.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude/THIRD_PARTY_NOTICES.md +4 -0
  2. package/.claude/agents/acceptance-tester.md +7 -2
  3. package/.claude/agents/code-reviewer.md +7 -2
  4. package/.claude/agents/domain-architect.md +7 -3
  5. package/.claude/agents/researcher.md +77 -0
  6. package/.claude/agents/ux-designer.md +42 -47
  7. package/.claude/hooks/validate-docs.ts +17 -1
  8. package/.claude/lib/architecture.ts +3 -33
  9. package/.claude/lib/backlog.ts +2 -0
  10. package/.claude/lib/config.ts +49 -0
  11. package/.claude/lib/discovery.ts +200 -32
  12. package/.claude/lib/engineering.ts +18 -6
  13. package/.claude/lib/gaps.ts +179 -0
  14. package/.claude/lib/glob.ts +39 -0
  15. package/.claude/lib/repo.ts +143 -19
  16. package/.claude/lib/research.ts +303 -0
  17. package/.claude/lib/slices.ts +529 -0
  18. package/.claude/package.json +1 -0
  19. package/.claude/settings.json +2 -2
  20. package/.claude/skills/adopt/SKILL.md +111 -34
  21. package/.claude/skills/adopt/scripts/gap-register.ts +61 -0
  22. package/.claude/skills/adopt/scripts/repo-map.ts +34 -2
  23. package/.claude/skills/adopt/scripts/slice-doc.ts +117 -0
  24. package/.claude/skills/adopt/scripts/slice.ts +163 -0
  25. package/.claude/skills/adopt/templates/context-map.as-is.md +22 -0
  26. package/.claude/skills/architect/SKILL.md +6 -1
  27. package/.claude/skills/backlog/SKILL.md +2 -2
  28. package/.claude/skills/bootstrap/SKILL.md +9 -7
  29. package/.claude/skills/domain-model/CONTEXT-FORMAT.md +1 -1
  30. package/.claude/skills/domain-model/SKILL.md +24 -4
  31. package/.claude/skills/event-storming/SKILL.md +3 -2
  32. package/.claude/skills/event-storming/schema/events.schema.json +2 -0
  33. package/.claude/skills/event-storming/templates/events.example.yaml +6 -0
  34. package/.claude/skills/feature/SKILL.md +16 -13
  35. package/.claude/skills/feature/scripts/intake.ts +1 -1
  36. package/.claude/skills/grilling/SKILL.md +15 -0
  37. package/.claude/skills/groom-backlog/SKILL.md +19 -17
  38. package/.claude/skills/implement/SKILL.md +199 -78
  39. package/.claude/skills/implement/scripts/characterize.ts +252 -0
  40. package/.claude/skills/implement/scripts/check-scenarios.ts +1 -1
  41. package/.claude/skills/implement/scripts/story-context.ts +65 -1
  42. package/.claude/skills/implement/scripts/verify.ts +4 -1
  43. package/.claude/skills/improve-codebase-architecture/SKILL.md +4 -1
  44. package/.claude/skills/prototype/SKILL.md +1 -1
  45. package/.claude/skills/research/SKILL.md +149 -0
  46. package/.claude/skills/research/scripts/research-check.ts +105 -0
  47. package/.claude/skills/research/templates/research.md +29 -0
  48. package/.claude/skills/review-stories/SKILL.md +49 -19
  49. package/.claude/skills/shape/SKILL.md +26 -7
  50. package/.claude/skills/tdd/SKILL.md +10 -0
  51. package/.claude/skills/user-stories/SKILL.md +128 -51
  52. package/.claude/skills/user-stories/schema/story.schema.json +10 -3
  53. package/.claude/skills/user-stories/scripts/lint-story.ts +56 -0
  54. package/.claude/skills/user-stories/templates/story-template.md +6 -2
  55. package/.claude/skills/ux-design/SKILL.md +9 -7
  56. package/.opencode/agents/acceptance-tester.md +8 -3
  57. package/.opencode/agents/code-reviewer.md +8 -3
  58. package/.opencode/agents/domain-architect.md +7 -3
  59. package/.opencode/agents/researcher.md +91 -0
  60. package/.opencode/agents/ux-designer.md +42 -52
  61. package/.opencode/commands/adopt.md +1 -1
  62. package/.opencode/commands/bootstrap.md +1 -1
  63. package/.opencode/commands/feature.md +1 -1
  64. package/.opencode/commands/groom-backlog.md +1 -1
  65. package/.opencode/commands/implement.md +1 -1
  66. package/.opencode/commands/research.md +7 -0
  67. package/.opencode/commands/review-stories.md +1 -1
  68. package/.opencode/commands/shape.md +1 -1
  69. package/AGENTS.md +30 -0
  70. package/CHANGELOG.md +73 -0
  71. package/README.md +108 -24
  72. package/guide/brownfield.md +41 -12
  73. package/guide/concepts.md +23 -6
  74. package/guide/greenfield.md +4 -3
  75. package/package.json +1 -1
  76. package/src/cli.mjs +42 -4
  77. package/src/detect.mjs +9 -0
  78. package/src/install.mjs +66 -6
  79. package/templates/docs/domain/context-map.md +5 -1
  80. package/templates/docs/domain/events.yaml +3 -0
  81. package/templates/docs/research/README.md +70 -0
  82. package/templates/docs/stories/README.md +12 -3
  83. package/templates/docs/workflow.md +76 -19
  84. package/.claude/agents/lead-dev.md +0 -52
  85. package/.claude/agents/product-owner.md +0 -39
  86. package/.claude/agents/requirements-engineer.md +0 -44
  87. package/.opencode/agents/lead-dev.md +0 -69
  88. package/.opencode/agents/product-owner.md +0 -59
  89. package/.opencode/agents/requirements-engineer.md +0 -58
@@ -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 ranking
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
- - `docs/domain/context-map.md`, `docs/architecture/data-model.md` (as-is)
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
- - **package docs** – `<package>/AGENTS.md` plus `<package>/CLAUDE.md`, written by
70
- `node .claude/skills/adopt/scripts/package-doc.ts <path>`
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` (and open
105
- questions in `OPEN_QUESTIONS.md`), so the model can be reviewed before anyone commits to work. Only
106
- `/adopt <scope> --stories` (or an explicit "turn these into stories") delegates the selected gaps to
107
- `requirements-engineer`.
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 <path>`.
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` | Bounded contexts and their relationships (DDD patterns) | domain-architect |
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 | requirements-engineer, PO, lead-dev; `/backlog-sync` with a tracker |
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 → in-progress → done` (plus
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
- - **At most 10 scenarios.** Each is one red → green cycle. More means split.
57
- - **A scenario title becomes a test title**: `ST-NNN: <scenario title>`. `verify.ts` checks the link.
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
 
@@ -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
- At most 10 scenarios per story; no implementation plan inside a domain story; every story links its `events`.
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` – one lead-dev pass over value, buildability and UX. The user approves every `ready`. With a tracker,
70
- pull before the review and push the approved stories afterwards (`/backlog-sync`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kataster",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
4
4
  "description": "Agent workflow for domain-driven discovery and delivery – drop it into a greenfield or brownfield repository and build the domain by grilling.",
5
5
  "keywords": [
6
6
  "agents",
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 one lead-dev pass before a story is ready
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/validate-stories.ts)",
62
- "Bash(node .claude/skills/user-stories/scripts/render-backlog.ts)",
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 content = read(file);
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), content);
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
- - **Traceability:** every scenario has a test titled \`ST-NNN: <scenario title>\`; domain IDs come from
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
- # Context map – {{project}}
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
- - Discovery ends at `ready`; `/implement` moves a story to `in-progress` and `done`.
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