@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.
- package/README.md +5 -2
- package/cli.js +53 -1
- package/package.json +2 -2
- package/shared/build-kit/lib/checks/README.md +59 -0
- package/shared/build-kit/lib/ralph.js +110 -28
- package/shared/build-kit/lib/util/find-slice.cjs +59 -0
- package/shared/build-kit/ralph-claude.js +6 -2
- package/shared/skills/learn-eventmodelers-api/SKILL.md +52 -46
- package/shared/skills/request-feedback/SKILL.md +7 -5
- package/stacks/axon/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/blank/templates/build-kit/CLAUDE.md +1 -1
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
- package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
- package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +9 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +124 -0
- package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/kurrent/templates/root/README.md +46 -0
- package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
- package/stacks/kurrent/templates/root/mvnw +259 -0
- package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
- package/stacks/kurrent/templates/root/pom.xml +152 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
- package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
- package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +0 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/references/examples.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-interview-protocol/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +13 -4
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +5 -5
- package/stacks/modeling-kit/templates/kit/AGENTS.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -2
- package/stacks/node/templates/build-kit/CLAUDE.md +23 -1
- package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
- package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
- package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
- package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/node/templates/root/.githooks/pre-commit +11 -0
- package/stacks/node/templates/root/package.json +2 -1
- package/stacks/node/templates/root/setup-env.sh +7 -1
- package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +103 -0
- package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/opencqrs/templates/root/README.md +42 -0
- package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
- package/stacks/opencqrs/templates/root/mvnw +259 -0
- package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
- package/stacks/opencqrs/templates/root/pom.xml +139 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
- package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
- package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
- package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +26 -1
- package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
- package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
- package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
- package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
- package/stacks/supabase/templates/root/package.json +2 -1
- package/stacks/supabase/templates/root/setup-env.sh +7 -1
- package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
- package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
- package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
- package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
- package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
- package/stacks/umadb/templates/root/README.md +48 -0
- package/stacks/umadb/templates/root/docker-compose.yml +29 -0
- package/stacks/umadb/templates/root/mvnw +259 -0
- package/stacks/umadb/templates/root/mvnw.cmd +149 -0
- package/stacks/umadb/templates/root/pom.xml +151 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
- package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
- package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
- 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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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(
|
|
283
|
-
|
|
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)
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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.
|
|
739
|
+
## 11. Invitations (Organization Membership) — Commands
|
|
740
740
|
|
|
741
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
759
|
-
**
|
|
760
|
-
**
|
|
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/
|
|
765
|
-
|
|
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
|
-
|
|
771
|
-
**
|
|
772
|
-
**Event emitted**: `RoleAssigned`
|
|
760
|
+
**Response**: `200` — invitation confirmed, membership created
|
|
761
|
+
**Errors**: `404` invitation not found
|
|
773
762
|
|
|
774
763
|
---
|
|
775
764
|
|
|
776
|
-
|
|
765
|
+
### DELETE `/api/org/:orga_id/invitations/:invitation_id`
|
|
766
|
+
Cancel a pending invitation.
|
|
777
767
|
|
|
778
|
-
|
|
768
|
+
**Response**: `200`/`204` on success
|
|
769
|
+
|
|
770
|
+
---
|
|
779
771
|
|
|
780
|
-
|
|
781
|
-
Group details. Filter: `?_id=groupId`
|
|
772
|
+
## 12. Invitations (Organization Membership) — Read Models
|
|
782
773
|
|
|
783
|
-
|
|
784
|
-
Pending invitations. Filter: `?_id=invitationId`
|
|
774
|
+
All require a Supabase JWT (`Authorization: Bearer`).
|
|
785
775
|
|
|
786
|
-
### GET `/api/
|
|
787
|
-
|
|
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/
|
|
790
|
-
|
|
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
|
-
###
|
|
905
|
+
### Invitation Events (`src/slices/organization/OrganizationEvent.ts`)
|
|
900
906
|
|
|
901
907
|
```typescript
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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/
|
|
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/
|
|
939
|
+
| `src/slices/organization/OrganizationEvent.ts` | Organization/invitation domain events |
|
|
934
940
|
| `backend/src/server.ts` | Route wiring, CORS, `/api/user` |
|