@eventmodelers/cli 1.0.45 → 1.0.47

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 (123) hide show
  1. package/README.md +5 -2
  2. package/cli.js +53 -1
  3. package/package.json +2 -2
  4. package/shared/build-kit/lib/checks/README.md +59 -0
  5. package/shared/build-kit/lib/ralph.js +110 -28
  6. package/shared/build-kit/lib/util/find-slice.cjs +59 -0
  7. package/shared/build-kit/ralph-claude.js +6 -2
  8. package/shared/skills/learn-eventmodelers-api/SKILL.md +52 -46
  9. package/shared/skills/request-feedback/SKILL.md +7 -5
  10. package/stacks/axon/templates/build-kit/CLAUDE.md +1 -1
  11. package/stacks/blank/templates/build-kit/CLAUDE.md +1 -1
  12. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
  13. package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
  14. package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +9 -0
  15. package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
  16. package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  17. package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
  18. package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
  19. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
  20. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
  21. package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
  22. package/stacks/kurrent/templates/build-kit/CLAUDE.md +124 -0
  23. package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
  24. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
  25. package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
  26. package/stacks/kurrent/templates/root/README.md +46 -0
  27. package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
  28. package/stacks/kurrent/templates/root/mvnw +259 -0
  29. package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
  30. package/stacks/kurrent/templates/root/pom.xml +152 -0
  31. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  32. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
  33. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
  34. package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
  35. package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
  36. package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +2 -2
  37. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +0 -1
  38. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +1 -1
  39. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/references/examples.md +2 -2
  40. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -2
  41. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
  42. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-interview-protocol/SKILL.md +1 -1
  43. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +4 -4
  44. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +1 -1
  45. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +1 -1
  46. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +1 -1
  47. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +13 -4
  48. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +2 -2
  49. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +5 -5
  50. package/stacks/modeling-kit/templates/kit/AGENTS.md +1 -1
  51. package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -2
  52. package/stacks/node/templates/build-kit/CLAUDE.md +23 -1
  53. package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
  54. package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  55. package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
  56. package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
  57. package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  58. package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  59. package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  60. package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  61. package/stacks/node/templates/root/.githooks/pre-commit +11 -0
  62. package/stacks/node/templates/root/package.json +2 -1
  63. package/stacks/node/templates/root/setup-env.sh +7 -1
  64. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
  65. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  66. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
  67. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
  68. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
  69. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
  70. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
  71. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
  72. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +103 -0
  73. package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
  74. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
  75. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
  76. package/stacks/opencqrs/templates/root/README.md +42 -0
  77. package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
  78. package/stacks/opencqrs/templates/root/mvnw +259 -0
  79. package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
  80. package/stacks/opencqrs/templates/root/pom.xml +139 -0
  81. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  82. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
  83. package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
  84. package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
  85. package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
  86. package/stacks/supabase/templates/build-kit/CLAUDE.md +26 -1
  87. package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
  88. package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  89. package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
  90. package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
  91. package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  92. package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  93. package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  94. package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  95. package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
  96. package/stacks/supabase/templates/root/package.json +2 -1
  97. package/stacks/supabase/templates/root/setup-env.sh +7 -1
  98. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
  99. package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
  100. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
  101. package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
  102. package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
  103. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
  104. package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
  105. package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
  106. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
  107. package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
  108. package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
  109. package/stacks/umadb/templates/root/README.md +48 -0
  110. package/stacks/umadb/templates/root/docker-compose.yml +29 -0
  111. package/stacks/umadb/templates/root/mvnw +259 -0
  112. package/stacks/umadb/templates/root/mvnw.cmd +149 -0
  113. package/stacks/umadb/templates/root/pom.xml +151 -0
  114. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
  115. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
  116. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
  117. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
  118. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
  119. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
  120. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
  121. package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
  122. package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
  123. package/stacks/umadb/templates/root/src/test/resources/application.properties +8 -0
package/README.md CHANGED
@@ -19,6 +19,9 @@ npx @eventmodelers/cli init --stack node # Node.js / TypeScript
19
19
  npx @eventmodelers/cli init --stack supabase # Supabase
20
20
  npx @eventmodelers/cli init --stack axon # Axon Framework (Java/Kotlin)
21
21
  npx @eventmodelers/cli init --stack cratis-csharp # Cratis (.NET/C#)
22
+ npx @eventmodelers/cli init --stack opencqrs # OpenCQRS (Java, EventSourcingDB)
23
+ npx @eventmodelers/cli init --stack umadb # UmaDB (Java)
24
+ npx @eventmodelers/cli init --stack kurrent # Kurrent (Java, KurrentDB)
22
25
  ```
23
26
 
24
27
  The installer prompts for your API token, Organization ID, and Board ID from [app.eventmodelers.ai/account](https://app.eventmodelers.ai/account), scaffolds the stack into your project, and writes `.eventmodelers/config.json` with your credentials.
@@ -73,7 +76,7 @@ your-project/
73
76
  └── CLAUDE.md ← agent instructions
74
77
  ```
75
78
 
76
- The four backend stacks (`node`, `supabase`, `axon`, `cratis-csharp`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
79
+ The seven backend stacks (`node`, `supabase`, `axon`, `cratis-csharp`, `opencqrs`, `umadb`, `kurrent`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
77
80
 
78
81
  ## Skills
79
82
 
@@ -94,7 +97,7 @@ Use skills in Claude Code with `/skill-name`:
94
97
  | `/update-slice-status` | Update slice status on the board |
95
98
  | `/load-slice` | Persist board slices to disk (backend stacks) |
96
99
  | `/build-state-change`, `/build-state-view`, `/build-automation`, `/build-webhook` | Implement a slice's command/view/automation/webhook (backend stacks) |
97
- | `/request-feedback` | Post a QUESTION comment and mark a slice `Blocked` when it's genuinely ambiguous (backend stacks) |
100
+ | `/request-feedback` | Post a comment and mark a slice `Blocked` when it's genuinely ambiguous (backend stacks) |
98
101
 
99
102
  Which skills install depends on the chosen stack — see `stacks/<name>/templates/.claude/skills/`. `/connect`, `/learn-eventmodelers-api`, `/update-slice-status`, and `/request-feedback` have no stack-specific content and install into every stack from `shared/skills/` instead.
100
103
 
package/cli.js CHANGED
@@ -70,6 +70,27 @@ const STACKS = {
70
70
  useShared: true,
71
71
  needsBoardId: true,
72
72
  },
73
+ opencqrs: {
74
+ label: 'OpenCQRS (Java, EventSourcingDB)',
75
+ kitSubdir: 'build-kit',
76
+ kitDirName: '.build-kit',
77
+ useShared: true,
78
+ needsBoardId: true,
79
+ },
80
+ umadb: {
81
+ label: 'UmaDB (Java)',
82
+ kitSubdir: 'build-kit',
83
+ kitDirName: '.build-kit',
84
+ useShared: true,
85
+ needsBoardId: true,
86
+ },
87
+ kurrent: {
88
+ label: 'Kurrent (Java, KurrentDB)',
89
+ kitSubdir: 'build-kit',
90
+ kitDirName: '.build-kit',
91
+ useShared: true,
92
+ needsBoardId: true,
93
+ },
73
94
  };
74
95
 
75
96
  // Not a stack — no backend scaffold, just skills + the agent loop. Installed via
@@ -774,7 +795,10 @@ async function installStack(stackKey, stackCfg, options = {}) {
774
795
  // first-installed kit added (e.g. node_modules/.idea from a build-kit install).
775
796
  const gitignoreDest = join(targetDir, '.gitignore');
776
797
  const priorGitignore = existsSync(gitignoreDest) ? readFileSync(gitignoreDest, 'utf-8') : null;
777
- copyDirContents(rootSrc, targetDir, { skip: ['CLAUDE.md'] });
798
+ // .githooks/ (the slice commit-scope guard) is opt-in via `init --hooks` — skipped
799
+ // here and handled explicitly below so a plain `init` never silently changes the
800
+ // project's git hook wiring.
801
+ copyDirContents(rootSrc, targetDir, { skip: ['CLAUDE.md', '.githooks'] });
778
802
  if (priorGitignore !== null && existsSync(gitignoreDest)) {
779
803
  const incoming = readFileSync(gitignoreDest, 'utf-8');
780
804
  const merged = mergeGitignoreLines(priorGitignore, incoming);
@@ -888,6 +912,31 @@ async function installStack(stackKey, stackCfg, options = {}) {
888
912
  }
889
913
  }
890
914
 
915
+ // --- 3b. Opt-in slice commit-scope guard (`init --hooks`) ---
916
+ // Skipped from the generic root copy above so a plain `init` never touches git's
917
+ // hook wiring; installed explicitly here only when requested.
918
+ if (options.hooks) {
919
+ const hooksSrc = join(rootSrc, '.githooks');
920
+ if (existsSync(hooksSrc)) {
921
+ copyDirContents(hooksSrc, join(targetDir, '.githooks'));
922
+ const preCommitHook = join(targetDir, '.githooks', 'pre-commit');
923
+ if (existsSync(preCommitHook)) {
924
+ // cpSync doesn't reliably carry over the executable bit across platforms,
925
+ // and git silently skips a non-executable hook.
926
+ try { execSync(`chmod +x "${preCommitHook}"`); } catch {}
927
+ }
928
+ try {
929
+ execSync('git rev-parse --git-dir', { cwd: targetDir, stdio: 'ignore' });
930
+ execSync('git config core.hooksPath .githooks', { cwd: targetDir });
931
+ console.log(' ✓ Installed .githooks/ and set core.hooksPath — commits touching src/slices/ are now scope-guarded');
932
+ } catch {
933
+ console.log(' ✓ Installed .githooks/ — run `git config core.hooksPath .githooks` once this directory is a git repo to activate it');
934
+ }
935
+ } else {
936
+ console.log(' ℹ️ --hooks was given but this stack ships no .githooks/ template — nothing to install');
937
+ }
938
+ }
939
+
891
940
  // --- 4. Install kit dependencies ---
892
941
  if (existsSync(join(kitDir, 'package.json'))) {
893
942
  console.log('📦 Installing kit dependencies...');
@@ -1539,6 +1588,7 @@ credentialFlags(program
1539
1588
  .option('--target <name>', `Bridge target framework (${Object.keys(BRIDGE_TARGETS).join(', ')}) — only meaningful with --bridge`)
1540
1589
  .option('--hook <command>', 'Persist a default shell command hook for `bridge` to run per batch of slice changes instead of Claude/Ollama (e.g. commit + push .slices/ for a CI pipeline to pick up) — only meaningful with --bridge. Can also be set per-run with `bridge --hook`.')
1541
1590
  .option('--build-kit', 'Install a blank build-kit scaffold (.build-kit/ + .claude/skills/build-*/SKILL.md placeholders, all TODO-marked) for a stack not built into this CLI yet — no fixed backend. Mutually exclusive with --stack/--modeling/--bridge.')
1591
+ .option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit, running .build-kit/lib/check-commit-scope.cjs) and wire it up via `git config core.hooksPath .githooks` — only meaningful with --stack (build-kit stacks). Off by default.')
1542
1592
  .option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
1543
1593
  .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json'))
1544
1594
  .action(async (opts, command) => {
@@ -1628,6 +1678,7 @@ credentialFlags(program
1628
1678
  force: opts.force,
1629
1679
  credentialOverrides: credentialOverridesFromOpts(opts),
1630
1680
  templatesSource: join(clonedDir, 'templates'),
1681
+ hooks: opts.hooks,
1631
1682
  });
1632
1683
  return;
1633
1684
  }
@@ -1639,6 +1690,7 @@ credentialFlags(program
1639
1690
  global: opts.global,
1640
1691
  force: opts.force,
1641
1692
  credentialOverrides: credentialOverridesFromOpts(opts),
1693
+ hooks: opts.hooks,
1642
1694
  });
1643
1695
  });
1644
1696
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.45",
4
- "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
3
+ "version": "1.0.47",
4
+ "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "eventmodelers": "cli.js"
@@ -0,0 +1,59 @@
1
+ # Commit-scope guard checks
2
+
3
+ Every `*.cjs` file in this folder is loaded and run automatically by
4
+ `../check-commit-scope.cjs` against the staged changeset of a slice commit
5
+ (a commit that touches `src/slices/{context}/{slice}/**`). Files run in
6
+ filename sort order — that's why they're numbered.
7
+
8
+ ## Adding a check
9
+
10
+ Create a new file here, e.g. `60-my-check.cjs`, exporting:
11
+
12
+ ```js
13
+ 'use strict';
14
+
15
+ module.exports = {
16
+ name: 'my-check', // short id, prefixed onto any violation it reports
17
+ skipIfAlreadyFailing: false, // optional: true = skip this check once an earlier
18
+ // one already found a violation (use for slow checks,
19
+ // e.g. a build/typecheck — no point running it if the
20
+ // commit is already going to be rejected)
21
+ run(ctx) {
22
+ // Inspect ctx and return an array of violations. No violations → return
23
+ // [] (or undefined/null).
24
+ return [
25
+ { path: 'src/slices/cart/AddItem/AddItemCommand.ts', reason: 'why this is a problem' },
26
+ ];
27
+ },
28
+ };
29
+ ```
30
+
31
+ ### `ctx` passed to every check
32
+
33
+ | field | type | meaning |
34
+ |-----------------|-----------------|-----------------------------------------------------------------------|
35
+ | `changes` | `{status, path}[]` | staged files — `status` is git's single-letter code (`A`/`M`/`D`/...) |
36
+ | `touchesSlice` | `boolean` | always `true` — the runner only loads checks once this is true |
37
+ | `repoRoot` | `string` | absolute path to the repo root (`git rev-parse --show-toplevel`) |
38
+ | `SLICE_PATTERN` | `RegExp` | matches a path inside a slice's own folder: `src/slices/{ctx}/{slice}/`|
39
+
40
+ ### Return value
41
+
42
+ An array of `{ path, reason }` objects — one per violation. `path` should be
43
+ the offending file (or a synthetic label like `'(slice.json)'` if the problem
44
+ isn't tied to one staged file). `reason` is a short, human-readable sentence
45
+ explaining what's wrong; the runner prefixes it with `[<check name>]`.
46
+
47
+ Return `[]`, `undefined`, or `null` when there's nothing to report. Throwing
48
+ is treated as a violation too (`[<check name>] threw: <message>`), so a check
49
+ doesn't need its own top-level try/catch.
50
+
51
+ ### Conventions used by the existing checks
52
+
53
+ - Prefer **not blocking** over a false positive when a check can't determine
54
+ the answer confidently (e.g. slice.json isn't found, or a file's shape isn't
55
+ recognized) — these are heuristics layered on top of the hard path-scope
56
+ rules, not a full compiler/schema validator.
57
+ - If two checks would flag the *same file*, only the first one to run reports
58
+ it (the runner dedupes by path) — so keep genuinely distinct concerns in
59
+ separate files rather than trying to avoid overlap yourself.
@@ -201,14 +201,28 @@ async function fetchAndPersistSlices(cfg, kitDir) {
201
201
  writeFileSync(ctxPath, JSON.stringify({ name: activeCtx }, null, 2), 'utf-8');
202
202
  }
203
203
 
204
- // Write per-context index.json and per-slice slice.json
204
+ // Write per-context index.json and per-slice slice.json.
205
+ //
206
+ // This endpoint (`/slicedata/slices`) is the CHEAP summary one — `{ id, title, status }`
207
+ // only, no commands/events/specifications/codeGen prompts. Its whole job here is to keep
208
+ // `status` fresh so hasPendingTasks/getFirstPlannedSliceTitle see live transitions; it must
209
+ // NEVER clobber the richer slice.json a full fetch (`/load-slice`, `eventmodelers fetch`,
210
+ // `eventmodelers listen`) already wrote for the same slice. So every write below merges
211
+ // onto whatever's already on disk — spreading the existing object first, the fresh summary
212
+ // fields second — instead of replacing it wholesale.
205
213
  for (const [contextSlug, { slices: ctxSlices }] of Object.entries(contexts)) {
206
214
  const contextDir = join(slicesDir, contextSlug);
207
215
  mkdirSync(contextDir, { recursive: true });
208
216
 
217
+ const indexPath = join(contextDir, 'index.json');
218
+ const existingIndex = existsSync(indexPath) ? JSON.parse(readFileSync(indexPath, 'utf-8')) : { slices: [] };
219
+ const existingById = new Map((existingIndex.slices ?? []).map((e) => [e.id, e]));
220
+
209
221
  const indexSlices = ctxSlices.map((s, i) => {
210
222
  const folder = (s.title ?? s.id).replaceAll(' ', '').toLowerCase();
223
+ const existing = existingById.get(s.id);
211
224
  return {
225
+ ...existing,
212
226
  id: s.id,
213
227
  slice: s.title,
214
228
  index: i,
@@ -216,16 +230,18 @@ async function fetchAndPersistSlices(cfg, kitDir) {
216
230
  contextSlug,
217
231
  folder,
218
232
  status: s.status,
219
- definition: { id: s.id, title: s.title, status: s.status },
233
+ definition: { ...existing?.definition, id: s.id, title: s.title, status: s.status },
220
234
  };
221
235
  });
222
- writeFileSync(join(contextDir, 'index.json'), JSON.stringify({ slices: indexSlices }, null, 2), 'utf-8');
236
+ writeFileSync(indexPath, JSON.stringify({ slices: indexSlices }, null, 2), 'utf-8');
223
237
 
224
238
  for (const slice of ctxSlices) {
225
239
  const folder = (slice.title ?? slice.id).replaceAll(' ', '').toLowerCase();
226
240
  const sliceDir = join(contextDir, folder);
227
241
  mkdirSync(sliceDir, { recursive: true });
228
- writeFileSync(join(sliceDir, 'slice.json'), JSON.stringify(slice, null, 2), 'utf-8');
242
+ const sliceJsonPath = join(sliceDir, 'slice.json');
243
+ const existingSlice = existsSync(sliceJsonPath) ? JSON.parse(readFileSync(sliceJsonPath, 'utf-8')) : {};
244
+ writeFileSync(sliceJsonPath, JSON.stringify({ ...existingSlice, ...slice }, null, 2), 'utf-8');
229
245
  }
230
246
  }
231
247
 
@@ -265,30 +281,83 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllSt
265
281
 
266
282
  const channelName = `board:${cfg.boardId}-slicechanged`;
267
283
  const realtime = await createRealtimeAdapter(cfg, realtimeToken);
268
- realtime.subscribe(
269
- channelName,
270
- {
271
- message: (payload) => {
272
- if (payload === 'Exit') {
273
- console.log('[agent] Received "Exit" — shutting down');
274
- process.exit(0);
284
+
285
+ // Shared by the scheduled timer, a CHANNEL_ERROR/TIMED_OUT subscribe status, and a
286
+ // 401 from the alive-ping — whichever notices the token is bad first wins; the rest
287
+ // just await the same in-flight refresh instead of firing duplicate mint requests.
288
+ const ts = () => new Date().toISOString();
289
+ let refreshing = null;
290
+ const refreshToken = (reason) => {
291
+ if (!refreshing) {
292
+ const startedAt = Date.now();
293
+ console.log(`[agent] ${ts()} Refreshing realtime token (reason: ${reason})...`);
294
+ refreshing = (async () => {
295
+ try {
296
+ realtimeToken = await retryOn401('getRealtimeToken (refresh)', () => getRealtimeToken(cfg));
297
+ await realtime.setAuth(realtimeToken);
298
+ console.log(`[agent] ${ts()} Token refreshed (reason: ${reason}, took ${Date.now() - startedAt}ms)`);
299
+ } catch (err) {
300
+ console.error(`[agent] ${ts()} Token refresh FAILED (reason: ${reason}):`, err);
301
+ throw err;
302
+ } finally {
303
+ refreshing = null;
275
304
  }
305
+ })();
306
+ }
307
+ return refreshing;
308
+ };
309
+
310
+ // A subscribe that errors on a stale token needs a fresh token AND a new join
311
+ // attempt — setAuth alone doesn't re-join a channel that already errored out.
312
+ // Capped so a non-expiry auth failure (e.g. genuinely revoked access) can't turn
313
+ // into a tight resubscribe loop hammering the platform forever.
314
+ let channelErrorStreak = 0;
315
+ const subscribeChannel = () => {
316
+ realtime.subscribe(
317
+ channelName,
318
+ {
319
+ message: (payload) => {
320
+ if (payload === 'Exit') {
321
+ console.log(`[agent] ${ts()} Received "Exit" — shutting down`);
322
+ process.exit(0);
323
+ }
324
+ },
325
+ 'slice:changed': (payload) => handleSliceChanged(payload, cfg, kitDir, queueAllStatuses),
276
326
  },
277
- 'slice:changed': (payload) => handleSliceChanged(payload, cfg, kitDir, queueAllStatuses),
278
- },
279
- (status) => console.log(`[agent] Channel "${channelName}": ${status}`),
280
- );
327
+ async (status) => {
328
+ if (status !== 'CHANNEL_ERROR' && status !== 'TIMED_OUT') {
329
+ if (channelErrorStreak > 0) {
330
+ console.log(`[agent] ${ts()} Channel "${channelName}": ${status} — recovered after ${channelErrorStreak} failed attempt(s)`);
331
+ } else {
332
+ console.log(`[agent] ${ts()} Channel "${channelName}": ${status}`);
333
+ }
334
+ channelErrorStreak = 0;
335
+ return;
336
+ }
337
+ channelErrorStreak += 1;
338
+ console.warn(`[agent] ${ts()} Channel "${channelName}": ${status} (attempt ${channelErrorStreak}/5)`);
339
+ if (channelErrorStreak > 5) {
340
+ console.error(`[agent] ${ts()} Channel "${channelName}" failed ${channelErrorStreak} times in a row — giving up until the next scheduled token refresh (every 10min)`);
341
+ return;
342
+ }
343
+ try {
344
+ await refreshToken(`channel ${status}`);
345
+ await new Promise((r) => setTimeout(r, 2_000));
346
+ console.log(`[agent] ${ts()} Resubscribing to channel "${channelName}" (attempt ${channelErrorStreak}/5)...`);
347
+ subscribeChannel();
348
+ } catch (err) {
349
+ console.error(`[agent] ${ts()} Token refresh after channel error failed, will not resubscribe this round:`, err);
350
+ }
351
+ },
352
+ );
353
+ };
354
+ subscribeChannel();
281
355
 
282
- setInterval(async () => {
283
- try {
284
- realtimeToken = await retryOn401('getRealtimeToken (refresh)', () => getRealtimeToken(cfg));
285
- await realtime.setAuth(realtimeToken);
286
- console.log('[agent] Token refreshed');
287
- } catch (err) {
288
- console.error('[agent] Token refresh failed:', err);
289
- }
356
+ setInterval(() => {
357
+ refreshToken('scheduled 10min refresh').catch((err) => console.error(`[agent] ${ts()} Scheduled token refresh failed:`, err));
290
358
  }, 10 * 60 * 1000);
291
359
 
360
+ let lastPingFailed = false;
292
361
  const ping = async () => {
293
362
  try {
294
363
  const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
@@ -297,9 +366,19 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllSt
297
366
  body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: agentType, agent_id: cfg.agentId }),
298
367
  signal: AbortSignal.timeout(10_000),
299
368
  });
300
- if (!res.ok) console.error(`[agent] Ping failed: ${res.status} ${await res.text().catch(() => '')}`);
369
+ if (!res.ok) {
370
+ console.error(`[agent] ${ts()} Ping failed: ${res.status} ${await res.text().catch(() => '')}`);
371
+ lastPingFailed = true;
372
+ if (res.status === 401) {
373
+ await refreshToken('ping-401').catch((err) => console.error(`[agent] ${ts()} Token refresh after 401 ping failed:`, err));
374
+ }
375
+ return;
376
+ }
377
+ if (lastPingFailed) console.log(`[agent] ${ts()} Ping recovered`);
378
+ lastPingFailed = false;
301
379
  } catch (err) {
302
- console.error('[agent] Ping error:', err);
380
+ console.error(`[agent] ${ts()} Ping error:`, err);
381
+ lastPingFailed = true;
303
382
  }
304
383
  };
305
384
  await ping();
@@ -354,10 +433,13 @@ async function runWithRetry(label, fn) {
354
433
  }
355
434
  }
356
435
 
357
- async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice) {
436
+ async function ralphLoop(kitDir, cfg, onTask, onPlannedSlice, localOnly = false) {
358
437
  const promptFile = join(kitDir, 'lib', 'prompt.md');
359
438
  const backendPromptFile = join(kitDir, 'lib', 'backend-prompt.md');
360
- const credentialed = hasCredentials(cfg);
439
+ // --local must mean zero board contact even when .eventmodelers/config.json
440
+ // happens to hold valid credentials — never let a locally-present token flip
441
+ // this back on.
442
+ const credentialed = !localOnly && hasCredentials(cfg);
361
443
  let lastIdleCtx;
362
444
 
363
445
  while (true) {
@@ -410,7 +492,7 @@ export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice, a
410
492
  // reaches out to the platform at all.
411
493
  if (localOnly || !hasCredentials(local)) {
412
494
  console.log(` mode: local-only (no platform sync)${localOnly ? ' — forced by --local' : ''}\n`);
413
- await ralphLoop(kitDir, local, onTask, onPlannedSlice);
495
+ await ralphLoop(kitDir, local, onTask, onPlannedSlice, localOnly);
414
496
  return;
415
497
  }
416
498
 
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ // Shared by checks that need to cross-reference code against the slice's own
4
+ // definition. Not a check itself — no `run(ctx)` interface here.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function normalize(s) {
10
+ return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
11
+ }
12
+
13
+ // Given the {context}/{SliceName} folder names used under src/slices/, find the
14
+ // matching slice.json under .build-kit/.slices/<contextSlug>/<sliceFolder>/. The
15
+ // two sides are slugified differently (see shared/skills/load-slice/SKILL.md —
16
+ // contextSlug is hyphenated, sliceFolder strips all spaces), so this matches on
17
+ // a normalized (lowercase, alphanumeric-only) form instead of exact equality.
18
+ // Returns null — never throws — when zero or more than one candidate matches;
19
+ // callers should treat that as "can't verify" and skip rather than block.
20
+ function findSliceJson(repoRoot, context, sliceName) {
21
+ const root = path.join(repoRoot, '.build-kit', '.slices');
22
+ if (!fs.existsSync(root)) return null;
23
+
24
+ const wantSlice = normalize(sliceName);
25
+ const candidates = [];
26
+
27
+ let contextDirs;
28
+ try {
29
+ contextDirs = fs.readdirSync(root, { withFileTypes: true });
30
+ } catch {
31
+ return null;
32
+ }
33
+
34
+ for (const contextDir of contextDirs) {
35
+ if (!contextDir.isDirectory()) continue;
36
+ const contextPath = path.join(root, contextDir.name);
37
+ let sliceDirs;
38
+ try {
39
+ sliceDirs = fs.readdirSync(contextPath, { withFileTypes: true });
40
+ } catch {
41
+ continue;
42
+ }
43
+ for (const sliceDir of sliceDirs) {
44
+ if (!sliceDir.isDirectory()) continue;
45
+ if (normalize(sliceDir.name) !== wantSlice) continue;
46
+ const slicePath = path.join(contextPath, sliceDir.name, 'slice.json');
47
+ if (fs.existsSync(slicePath)) candidates.push(slicePath);
48
+ }
49
+ }
50
+
51
+ if (candidates.length !== 1) return null;
52
+ try {
53
+ return JSON.parse(fs.readFileSync(candidates[0], 'utf8'));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ module.exports = { findSliceJson, normalize };
@@ -11,7 +11,11 @@ const kitDir = dirname(fileURLToPath(import.meta.url));
11
11
  const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
12
12
 
13
13
  const cfg = loadLocalConfig(kitDir);
14
- const inlineHeader = cfg.boardId
14
+ const localOnly = process.env.RALPH_LOCAL === '1';
15
+ // --local must mean zero board contact — never hand Claude live board
16
+ // credentials via the inline header, even if config.json has them, or it'll
17
+ // treat them as already-connected and skip straight to board sync.
18
+ const inlineHeader = !localOnly && cfg.boardId
15
19
  ? `board=${cfg.boardId} token=${cfg.token} org=${cfg.organizationId} baseUrl=${cfg.baseUrl}\n\n`
16
20
  : '';
17
21
 
@@ -96,7 +100,7 @@ startRalph({
96
100
  projectDir,
97
101
  onTask: runClaude,
98
102
  onPlannedSlice: runClaude,
99
- localOnly: process.env.RALPH_LOCAL === '1',
103
+ localOnly,
100
104
  }).catch((err) => {
101
105
  console.error('[ralph] Fatal:', err);
102
106
  process.exit(1);
@@ -54,7 +54,7 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
54
54
  | `set_connection` | `boardId`, `source`, `target`, `action` (`'connect'\|'remove'`) | Add or remove a type-checked directed edge. Batch form `set_connections` takes `connections[]` (applied in order) plus `compact?` — `compact: true` returns a `{connected, existed, removed, notFound, failed, errors}` tally instead of one row per edge | — (via `edges` on §3 events) |
55
55
  | `auto_connect_node` | `boardId`, `nodeId` | Re-run auto-connect for a node | §3 `POST .../nodes/:nodeId/auto-connect` |
56
56
  | `link_element` | `boardId`, `nodeId`, `targetNodeId` | Link two existing same-type nodes: `targetNodeId` is replaced with a full copy of `nodeId`'s meta plus `meta.linkedTo`. Linking means first create, then link | §3 `POST .../nodes/:nodeId/link` |
57
- | `add_comment` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'\|'QUESTION'`), `author?` | Add a comment — `QUESTION` flags gaps/edge cases during review | — (via comment events) |
57
+ | `add_comment` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'`), `author?` | Add a comment — word the `text` as a question to flag gaps/edge cases during review; there is no separate `QUESTION` type | — (via comment events) |
58
58
  | `update_comment` | `boardId`, `nodeId`, `commentId`, `action` (`'resolve'\|'delete'`) | Resolve or delete a comment | — (via comment events) |
59
59
  | `create_screen` | `boardId`, `contentType` (`'image'\|'sketch'\|'html'`), `nodeId?`, `chapterId`, `cellId?`/`cellName?`, plus content fields (`imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`), `description?`, `fields?`, `autoConnect?` | Create + place a new screen node (SCREEN or HTML_SCREEN) atomically, in one call. Batch form `create_screens` takes `screens[]` (HTML only) + `autoConnect?`. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
60
60
  | `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages` | §4 `POST .../images/:id/sketch` + `image-nodes` |
@@ -66,7 +66,7 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
66
66
  | `commit_board_to_git` | `boardId` | Force a git-extension commit/push, bypassing the autoCommit gate | — (MCP-only; git extension) |
67
67
  | `update_prompt_status` | `promptId`, `newStatus`, `comment?` | Update a prompt's lifecycle status (`ADDED`/`CLAIMED`/`IN_PROGRESS`/`DONE`), optionally with a progress comment. Not board-scoped — no `boardId` arg; the prompt's board is resolved server-side. | §14 `POST .../prompts/:id/status` |
68
68
 
69
- **Not exposed via MCP at all** — always use REST/curl for these: §7 Config Import, §10 Snapshots, §11–12 User Management, §13 Utility (`/api/user`, swagger), and the rest of §14 Prompts (submission, claiming, deletion, realtime-token) — only the status-update endpoint has an MCP tool (`update_prompt_status`, used by the `update-prompt-status` skill); everything else in Prompts is an intentionally separate lifecycle the board-content MCP server doesn't otherwise own.
69
+ **Not exposed via MCP at all** — always use REST/curl for these: §7 Config Import, §10 Snapshots, §11–12 Invitations, §13 Utility (`/api/user`, swagger), and the rest of §14 Prompts (submission, claiming, deletion, realtime-token) — only the status-update endpoint has an MCP tool (`update_prompt_status`, used by the `update-prompt-status` skill); everything else in Prompts is an intentionally separate lifecycle the board-content MCP server doesn't otherwise own.
70
70
 
71
71
  **Capabilities with no direct MCP filter** — e.g. REST's `GET .../nodes?cellId=<id>&timelineId=<id>` and `?colId=<id>&timelineId=<id>` (§3) have no equivalent params on `get_nodes`. Either call the REST endpoint directly, or get the same answer by calling `get_node` on the CHAPTER and reading `meta.timelineData.cells` (sparse array; a cell absent from it is empty) instead of asking the server to filter by cell/column.
72
72
 
@@ -736,58 +736,48 @@ Delete a snapshot.
736
736
 
737
737
  ---
738
738
 
739
- ## 11. User Management — Commands (Event Sourced)
739
+ ## 11. Invitations (Organization Membership) — Commands
740
740
 
741
- All commands respond with:
742
- ```typescript
743
- {
744
- ok: true
745
- next_expected_stream_version: number
746
- last_event_global_position: number
747
- }
748
- ```
749
-
750
- Optional headers on all: `correlation_id`, `causation_id`
741
+ **Files**: `src/slices/organization/InviteUser/routes.ts`, `src/slices/organization/ConfirmInvitation/routes.ts`, `src/slices/organization/DeleteInvitation/routes.ts`
751
742
 
752
- ### POST `/api/creategroup`
753
- **Body**: `{ groupId: string, name: string }`
754
- **Event emitted**: `GroupCreated`
743
+ There is no generic "group"/"role-assignment" API — an organization *is* the group, and a
744
+ member's role is set once, at invite time (there is no separate call to change an existing
745
+ member's role afterward). All of these require a Supabase JWT (`Authorization: Bearer`), never
746
+ a bot `x-token`.
755
747
 
756
- ---
748
+ ### POST `/api/org/:orga_id/invitations`
749
+ Invite a user to join an organization with a given role. Caller must already be an `admin` of `orga_id`.
757
750
 
758
- ### POST `/api/inviteuser`
759
- **Body**: `{ groupId: string, email: string, invitationId: string }`
760
- **Event emitted**: `UserInvited`
751
+ **Body**: `{ role: string, email: string, description?: string }`
752
+ **Response**: `201` invitation created
753
+ **Errors**: `400` role/email missing · `403` caller is not an org admin · `409` user already invited or already a member
761
754
 
762
755
  ---
763
756
 
764
- ### POST `/api/acceptinvite`
765
- **Body**: `{ userId: string, groupId: string, invitationId: string }`
766
- **Event emitted**: `InvitationAccepted`
767
-
768
- ---
757
+ ### POST `/api/invitations/:token/confirm`
758
+ Accept an invitation — `token` is the invitation's own token (from the invite email link), not an API token. The confirming user's own account email must match the invited email.
769
759
 
770
- ### POST `/api/assignrole`
771
- **Body**: `{ userId: string, groupId: string, role: string }`
772
- **Event emitted**: `RoleAssigned`
760
+ **Response**: `200` — invitation confirmed, membership created
761
+ **Errors**: `404` invitation not found
773
762
 
774
763
  ---
775
764
 
776
- ## 12. User Management — Read Models (Projections)
765
+ ### DELETE `/api/org/:orga_id/invitations/:invitation_id`
766
+ Cancel a pending invitation.
777
767
 
778
- All require authentication. Optional query param `_id` to filter by ID.
768
+ **Response**: `200`/`204` on success
769
+
770
+ ---
779
771
 
780
- ### GET `/api/query/group-details-lookup`
781
- Group details. Filter: `?_id=groupId`
772
+ ## 12. Invitations (Organization Membership) — Read Models
782
773
 
783
- ### GET `/api/query/open-invites`
784
- Pending invitations. Filter: `?_id=invitationId`
774
+ All require a Supabase JWT (`Authorization: Bearer`).
785
775
 
786
- ### GET `/api/query/user-group-assignments`
787
- User-to-group mappings. Filter: `?_id=groupId`
776
+ ### GET `/api/user-organizations`
777
+ Organizations (and the caller's role in each) that the authenticated user belongs to.
788
778
 
789
- ### GET `/api/query/users-to-assign-to-groups`
790
- Users available for group assignment. Filter: `?_id=userId`
779
+ ### GET `/api/client/org/:orgId/invitations`
780
+ Pending invitations for an organization (client-facing list — used by the org settings UI).
791
781
 
792
782
  ---
793
783
 
@@ -883,6 +873,22 @@ Exchange an `x-token` for a short-lived Supabase-compatible JWT, used to subscri
883
873
 
884
874
  ---
885
875
 
876
+ ### POST `/api/agent-alive`
877
+ Record a heartbeat ping for a running modeling/build agent. Auth: Supabase JWT (`Authorization: Bearer`) — exchange the `x-token` for one first via `GET /api/org/:orgId/prompts/realtime-token` above; a raw `x-token` alone is not accepted here.
878
+
879
+ **Body**: `{ token: string, board_id?: string, agent_type: 'MODELING' | 'BUILD', agent_id: string }`
880
+ **Response**: `200` — `{ ok: true }`
881
+ **Errors**: `400` `agent_id`/`agent_type` missing · `404` token not found
882
+
883
+ ---
884
+
885
+ ### GET `/api/org/:orgId/boards/:boardId/agent-alive`
886
+ Check whether an agent has pinged for a board within the last 45s. Auth: `x-token` (bot) or a Supabase JWT (`Authorization: Bearer`) — either works.
887
+
888
+ **Response**: `200` — `{ alive: boolean, agentTypes: string[] }`
889
+
890
+ ---
891
+
886
892
  ## Domain Events
887
893
 
888
894
  ### Snapshot Events (`src/events/SnapshotsEvents.ts`)
@@ -896,14 +902,14 @@ SnapshotShared // { id }
896
902
  SnapshotPublished // { id, payloadId, bucket, path }
897
903
  ```
898
904
 
899
- ### User Management Events (`src/events/UserManagementEvents.ts`)
905
+ ### Invitation Events (`src/slices/organization/OrganizationEvent.ts`)
900
906
 
901
907
  ```typescript
902
- GroupCreated // { groupId, owner, name }
903
- UserAssignedToGroup // { groupId, userId }
904
- UserInvited // { groupId, invitationId, email }
905
- InvitationAccepted // { invitationId, groupId, userId }
906
- RoleAssigned // { groupId, userId, role }
908
+ UserInvited // { orgaId, userId, role, invitationId, token, description?, boardId?, email? }
909
+ InvitationConfirmed // { orgaId, userId, invitationId, token?, email? }
910
+ InvitationDeleted // { invitationId }
911
+ UserAssignedToOrganization // { id, userId, orgaId, role?, boardId?, email? }
912
+ UserRemovedFromOrganization // { orgaId, userId }
907
913
  ```
908
914
 
909
915
  All events support optional metadata: `user_id`, `correlation_id`, `causation_id`
@@ -928,7 +934,7 @@ All events support optional metadata: `user_id`, `correlation_id`, `causation_id
928
934
  | `src/slices/slicedata/routes.ts` | Slice data read models |
929
935
  | `src/slices/extensions/routes.ts` | Extension management |
930
936
  | `src/slices/Snapshots/routes.ts` | Snapshot CRUD |
931
- | `src/slices/usermanagement/*/routes*.ts` | User management commands + projections |
937
+ | `src/slices/organization/InviteUser/routes.ts`, `ConfirmInvitation/routes.ts`, `DeleteInvitation/routes.ts` | Organization invitation commands |
932
938
  | `src/events/SnapshotsEvents.ts` | Snapshot domain events |
933
- | `src/events/UserManagementEvents.ts` | User management domain events |
939
+ | `src/slices/organization/OrganizationEvent.ts` | Organization/invitation domain events |
934
940
  | `backend/src/server.ts` | Route wiring, CORS, `/api/user` |