@eventmodelers/cli 1.0.54 → 1.0.56
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 +6 -0
- package/cli.js +24 -0
- package/package.json +1 -1
- package/shared/build-kit/lib/ralph.js +103 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +3 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +5 -1
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +90 -2
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +81 -3
- package/stacks/node/templates/build-kit/CLAUDE.md +6 -1
- package/stacks/node/templates/build-kit/lib/checks/60-openapi-annotation.cjs +72 -0
- package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +42 -0
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +43 -0
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +42 -0
- package/stacks/react/templates/build-kit/CLAUDE.md +62 -0
- package/stacks/react/templates/build-kit/README.md +79 -0
- package/stacks/react/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +135 -0
- package/stacks/react/templates/build-kit/lib/prompt.md +139 -0
- package/stacks/react/templates/build-kit/lib/ralph.js +508 -0
- package/stacks/react/templates/build-kit/package.json +9 -0
- package/stacks/react/templates/build-kit/ralph-claude.js +107 -0
- package/stacks/react/templates/build-kit/ralph-ollama.js +40 -0
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +5 -1
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +90 -2
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +80 -2
- package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +73 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +6 -1
- package/stacks/supabase/templates/build-kit/lib/checks/60-openapi-annotation.cjs +72 -0
- package/stacks/supabase/templates/root/src/swagger.ts +3 -1
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +305 -0
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +238 -0
- package/stacks/supabase-react/templates/.claude/skills/init-style-guide/SKILL.md +60 -0
- package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/SKILL.md +28 -0
- package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/references/README.md +5 -0
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +149 -0
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +139 -0
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +145 -0
- package/stacks/supabase-react/templates/root/.env.example +12 -0
- package/stacks/supabase-react/templates/root/.oxlintrc.json +9 -0
- package/stacks/supabase-react/templates/root/README.md +49 -0
- package/stacks/supabase-react/templates/root/index.html +13 -0
- package/stacks/supabase-react/templates/root/package.json +26 -0
- package/stacks/supabase-react/templates/root/public/favicon.svg +1 -0
- package/stacks/supabase-react/templates/root/public/icons.svg +24 -0
- package/stacks/supabase-react/templates/root/src/App.css +184 -0
- package/stacks/supabase-react/templates/root/src/App.tsx +122 -0
- package/stacks/supabase-react/templates/root/src/assets/hero.png +0 -0
- package/stacks/supabase-react/templates/root/src/assets/react.svg +1 -0
- package/stacks/supabase-react/templates/root/src/assets/vite.svg +1 -0
- package/stacks/supabase-react/templates/root/src/index.css +111 -0
- package/stacks/supabase-react/templates/root/src/lib/api.ts +127 -0
- package/stacks/supabase-react/templates/root/src/lib/supabase.ts +10 -0
- package/stacks/supabase-react/templates/root/src/main.tsx +10 -0
- package/stacks/supabase-react/templates/root/src/slices/.gitkeep +0 -0
- package/stacks/supabase-react/templates/root/src/vite-env.d.ts +13 -0
- package/stacks/supabase-react/templates/root/tsconfig.app.json +26 -0
- package/stacks/supabase-react/templates/root/tsconfig.json +7 -0
- package/stacks/supabase-react/templates/root/tsconfig.node.json +23 -0
- package/stacks/supabase-react/templates/root/vite.config.ts +7 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Project Configuration
|
|
2
|
+
|
|
3
|
+
Read `src/slices` to understand the global structure — each subdirectory is one board slice; there is no other domain code in this project.
|
|
4
|
+
|
|
5
|
+
This stack is UI-only: it only builds **state-change** (command → event, write side) and
|
|
6
|
+
**state-view** (read model, read side) slices. Translation slices default to
|
|
7
|
+
`build-state-change`. Automations/processors have no UI counterpart here — if a slice's
|
|
8
|
+
`processors` array is non-empty, that slice belongs to a backend stack, not this one; do
|
|
9
|
+
not attempt to build it, flag it via `request-feedback` instead.
|
|
10
|
+
|
|
11
|
+
## File Structure Constraints
|
|
12
|
+
|
|
13
|
+
- **Strict Path Limitation**: `src/slices/{contextSlug}/{laneSlug}/{slicename}/*` — the one
|
|
14
|
+
exception is `src/pages/`, where multiple slices sharing one screen get composed together (see
|
|
15
|
+
guideline 8 below)
|
|
16
|
+
- **Slice Organization**: Each feature/domain should be organized as a separate slice
|
|
17
|
+
|
|
18
|
+
## Code Standards
|
|
19
|
+
|
|
20
|
+
- **Language**: TypeScript
|
|
21
|
+
- **Module System**: ESM
|
|
22
|
+
- **Type Safety**: TypeScript, strict mode (see `tsconfig.app.json`)
|
|
23
|
+
|
|
24
|
+
## Development Guidelines
|
|
25
|
+
|
|
26
|
+
1. Each slice should be self-contained and focused on a specific domain
|
|
27
|
+
2. Maintain clear separation of concerns within each slice
|
|
28
|
+
3. Headers, auth, `fetch`, and Supabase table access live exclusively in `src/lib/api.ts` — a
|
|
29
|
+
slice component never imports `src/lib/supabase.ts` itself, never calls `fetch`, and never
|
|
30
|
+
calls `supabase.from(...)` directly; it calls `postCommand`/`queryReadModel` and only contains
|
|
31
|
+
business logic (validation, request-body shaping, response handling)
|
|
32
|
+
4. A command's/read-model's screen markup (`meta.pages` on its actor HTML_SCREEN node, scoped to
|
|
33
|
+
this slice's `meta.marks` when the screen is shared) is the visual blueprint — translate it into
|
|
34
|
+
JSX rather than redesigning it; see `build-state-change`/`build-state-view` Step 2
|
|
35
|
+
5. A slice's business logic lives in its component by default (plain `useState`, no ceremony). Only
|
|
36
|
+
split it into a colocated `use<Name>.ts` hook when that's genuinely warranted — reused by more
|
|
37
|
+
than one sibling component, or non-rendering logic large enough that separating it clearly helps
|
|
38
|
+
— never by default or "for consistency"; see `build-state-change`/`build-state-view` Step 3.
|
|
39
|
+
6. **Design every slice for deletability**: its folder must be removable wholesale without
|
|
40
|
+
leaving dangling imports elsewhere.
|
|
41
|
+
- Local state shared only *within* one slice's sub-components (e.g. a multi-step command, a
|
|
42
|
+
list + submit bar) — and *only* when a single component's own `useState` genuinely can't hold
|
|
43
|
+
it — gets its own `<Name>Context.tsx`, colocated in that same slice folder, never in
|
|
44
|
+
`src/lib`, never imported by another slice. This is the exception, not the default; most
|
|
45
|
+
slices need no Context at all. Follow the same `createContext`/`XProvider`/`useX` shape used
|
|
46
|
+
in this pattern's origin, `miro-eventmodeling` (e.g. `useReadOnly`/`ReadOnlyProvider`,
|
|
47
|
+
`useUser`/`UserProvider`): a context object, a `<Name>Provider>` component owning the state,
|
|
48
|
+
and a `use<Name>` hook wrapping `useContext` — throwing if the value is missing when there's
|
|
49
|
+
no sensible default (slice-scoped state always throws), returning a plain default only for
|
|
50
|
+
something genuinely app-wide.
|
|
51
|
+
- Only concerns that are truly cross-cutting (current session, theme, feature flags — things
|
|
52
|
+
every slice might need) get a shared provider under `src/lib/`, composed once where the app
|
|
53
|
+
mounts its providers (e.g. `src/main.tsx`), the same way `miro-eventmodeling`'s root layout
|
|
54
|
+
nests `UserProvider`/`OrganizationProvider`/etc.
|
|
55
|
+
- A slice component must never import from another slice's folder — the one place that's allowed
|
|
56
|
+
to is `src/pages/` (guideline 7). If two slices seem to need the same state, that state belongs
|
|
57
|
+
in a `src/lib` provider, not a cross-slice import.
|
|
58
|
+
7. **A screen's title is the page name.** Several slices can each hold their own copy of one shared
|
|
59
|
+
visual screen (one node per slice, each with a different mark, per the `html-screen` skill) —
|
|
60
|
+
when their screens share a `title`, their components belong on one real page together, arranged
|
|
61
|
+
as the marks show. The count of *distinct* screen titles across the board is a direct hint at how
|
|
62
|
+
many pages this app needs, not one page per slice. That composition lives at
|
|
63
|
+
`src/pages/<ScreenTitle>.tsx`, translating the screen's whole unmarked markup into JSX and
|
|
64
|
+
substituting each marked position with that slice's actual component; see
|
|
65
|
+
`build-state-change`/`build-state-view`'s page-composition step for the full procedure.
|
|
66
|
+
8. **A screen's lane is reflected in the folder.** The actor row (lane) a slice's screen is placed
|
|
67
|
+
in on the board — e.g. "Admin" vs. "Customer" — becomes a folder segment between context and
|
|
68
|
+
slice: `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/`. Resolve it via
|
|
69
|
+
`mcp__eventmodelers__get_board_outline` on the screen's chapter (returns `{id, type, title,
|
|
70
|
+
lane}` per node); see `build-state-change`/`build-state-view` Step 2. Omit the segment only for
|
|
71
|
+
the rare slice with no screen at all.
|
|
72
|
+
9. **Every component is testable in isolation, with samples.** A component must render sensibly
|
|
73
|
+
with no data (an empty/background state, not a crash), and must be exercisable with no real
|
|
74
|
+
backend running. Every slice ships a colocated `samples/sample-<N>.json` per numbered scenario
|
|
75
|
+
(`sample-1.json`, `sample-2.json`, ...) — for a STATE_VIEW component, shaped exactly like the
|
|
76
|
+
row(s) its `queryReadModel` call returns; for a STATE_CHANGE component, a full set of prop
|
|
77
|
+
values. `src/lib/api.ts`'s `postCommand`/`queryReadModel` are mock-aware: set
|
|
78
|
+
`VITE_DATA_MODE=mock` and every read is served from `samples/sample-<VITE_MOCK_SAMPLE>.json`
|
|
79
|
+
instead of Supabase, and every command POST resolves locally instead of hitting the network —
|
|
80
|
+
flipping one env var is the "switch between samples" mechanism, nothing per-component to wire.
|
|
81
|
+
The same number across different components' samples represents one coherent scenario across
|
|
82
|
+
the app, not an arbitrary per-component index. See `build-state-change`/`build-state-view`'s
|
|
83
|
+
samples step for the full convention.
|
|
84
|
+
|
|
85
|
+
At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project learnings.
|
|
86
|
+
|
|
87
|
+
When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before doing anything else.
|
|
88
|
+
|
|
89
|
+
## Building a Slice
|
|
90
|
+
|
|
91
|
+
**CRITICAL: You MUST always use the provided skills to build slices. NEVER implement a slice manually.**
|
|
92
|
+
**ALL fields, event names, command names, and business rules MUST come exclusively from slice.json. Do NOT invent, assume, or guess any field or logic not present in the slice definition.**
|
|
93
|
+
|
|
94
|
+
**If, at any point below, the slice's requirements are genuinely ambiguous, contradictory, or missing
|
|
95
|
+
a decision you need in order to proceed — do not guess, and do not build anyway.** Invoke the
|
|
96
|
+
`request-feedback` skill with the specific question; it posts the question as a comment on the slice
|
|
97
|
+
and marks it `Blocked`, and you then stop work on this slice for this run. This is an escalation path,
|
|
98
|
+
not a routine step — read `slice.json` and the matching build skill's own instructions fully first;
|
|
99
|
+
most slices are fully specified and need none of this.
|
|
100
|
+
|
|
101
|
+
**Exception: a missing `apiEndpoint`** (no backend endpoint/table decided yet) is a partial version of
|
|
102
|
+
this — flag it, but don't fully stop. `build-state-change`/`build-state-view` Step 1 cover the exact
|
|
103
|
+
mechanics: post a comment on the slice and mark it `Blocked` (same as `request-feedback`), but then
|
|
104
|
+
keep going — build the slice normally against a provisional mock path/table name with meaningful
|
|
105
|
+
sample data. The slice ends this run `Blocked` (step 6 below does not flip it to `Done`), but the code
|
|
106
|
+
is there, mocked, ready to be pointed at the real API once one exists.
|
|
107
|
+
|
|
108
|
+
When asked to build a slice, always follow this flow:
|
|
109
|
+
|
|
110
|
+
1. Read the slice definition from `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`.
|
|
111
|
+
2. Determine the slice type from `sliceType`:
|
|
112
|
+
- **`STATE_CHANGE`** → invoke `/build-state-change`
|
|
113
|
+
- **`STATE_VIEW`** → invoke `/build-state-view`
|
|
114
|
+
- **`AUTOMATION`** → not this stack's job (no UI counterpart) — invoke `request-feedback`
|
|
115
|
+
rather than building it; it belongs to whichever backend stack is installed alongside this one
|
|
116
|
+
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
117
|
+
4. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
118
|
+
5. Run `npm run build` and `npm run lint` (both must pass clean), then the slice's own tests only, if any exist.
|
|
119
|
+
6. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done` — **unless** the
|
|
120
|
+
matching build skill flagged a missing `apiEndpoint` and already marked the slice `Blocked`
|
|
121
|
+
(Step 1 exception above); in that case, commit as normal but leave the status `Blocked`.
|
|
122
|
+
|
|
123
|
+
After you are done, automatically run the tests for the slice that was edited.
|
|
124
|
+
|
|
125
|
+
## Example Slice Structure
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
src/lib/supabase.ts # Supabase client singleton — imported ONLY by api.ts
|
|
129
|
+
src/lib/api.ts # postCommand/queryReadModel + auth headers + mock-mode switch (shared, not per-slice)
|
|
130
|
+
src/lib/<CrossCutting>Provider.tsx # only for concerns every slice might need (session, theme, ...)
|
|
131
|
+
src/pages/<ScreenTitle>.tsx # only when >1 slice shares a screen title — composes their components
|
|
132
|
+
src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/
|
|
133
|
+
<CommandTitle>.tsx # STATE_CHANGE slice — component (logic inline by default)
|
|
134
|
+
use<CommandTitle>.ts # only if the logic was genuinely worth splitting out
|
|
135
|
+
<CommandTitle>Context.tsx # only if sibling components must share state
|
|
136
|
+
<ReadModelTitle>.tsx # STATE_VIEW slice — component (logic inline by default)
|
|
137
|
+
samples/
|
|
138
|
+
sample-1.json # one numbered scenario per file — props (STATE_CHANGE) or query result (STATE_VIEW)
|
|
139
|
+
sample-2.json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Everything under one `<sliceFolder>/` is deletable as a unit — nothing outside it should import
|
|
143
|
+
from it except the parent that renders the slice's top-level component, or `src/pages/` when this
|
|
144
|
+
slice's screen is composed into a shared page.
|
|
145
|
+
|
|
146
|
+
`contextSlug`/`sliceFolder` are derived exactly as in the `load-slice` skill, and `laneSlug` per
|
|
147
|
+
guideline 8 above — so a slice's frontend code nests one level deeper than the staging data at
|
|
148
|
+
`.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json` (which has no lane concept), rather
|
|
149
|
+
than mirroring it 1:1.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Agent Learnings
|
|
2
|
+
|
|
3
|
+
Patterns and gotchas discovered during task processing. Update this file whenever you encounter something reusable.
|
|
4
|
+
|
|
5
|
+
## tasks.json
|
|
6
|
+
|
|
7
|
+
- Tasks are objects with `id`, `createdAt`, and `payload` (a `SliceChangedPayload`).
|
|
8
|
+
- After completing a task, remove it from the array entirely — do not add a status field.
|
|
9
|
+
- Write `[]` to `tasks.json` if the last task is completed.
|
|
10
|
+
|
|
11
|
+
## SliceChangedPayload fields
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
event always "slice:changed"
|
|
15
|
+
organizationId org UUID or null
|
|
16
|
+
boardId board UUID
|
|
17
|
+
sliceId SLICE_BORDER node UUID — use this with /load-slice
|
|
18
|
+
sliceTitle human-readable slice name (may be null)
|
|
19
|
+
sliceStatus e.g. "Created", "InProgress", "Done", "Blocked" (may be null)
|
|
20
|
+
timestamp unix ms when the change was emitted
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Slice files
|
|
24
|
+
|
|
25
|
+
The realtime agent writes one file per slice on startup and after each `slice:changed` event:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
.slices/<context>/<sliceName>.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `<context>` is the slice's context value, or `default` if none.
|
|
32
|
+
- `<sliceName>` is the slice title lowercased with spaces and the `"slice:"` prefix removed (e.g. `"slice: Enable User"` → `enableuser`).
|
|
33
|
+
|
|
34
|
+
These files are always up to date — read them directly before invoking any skill.
|
|
35
|
+
|
|
36
|
+
## Skill Usage
|
|
37
|
+
|
|
38
|
+
- Always run `/connect` first to load credentials from `.eventmodelers/config.json` before calling any other skill.
|
|
39
|
+
- `/load-slice sliceId=<uuid>` re-fetches all slices from the API, refreshes the slice files, and returns the requested slice. Use it when you need a guaranteed-fresh view of a specific slice.
|
|
40
|
+
- Read `.slices/<context>/<sliceName>.json` directly when you already know the context and name and the file is recent enough.
|
|
41
|
+
|
|
42
|
+
## Board API
|
|
43
|
+
|
|
44
|
+
- The `boardId` and `organizationId` from each payload provide full context — pass them to skills.
|
|
45
|
+
- Node events use `node:created`, `node:changed`, `node:deleted` — always POST to `/api/org/:orgId/boards/:boardId/nodes/events`.
|
|
46
|
+
- Slice metadata (title, status) lives on the SLICE_BORDER node under `meta.sliceStatus` and `meta.title`.
|
|
47
|
+
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Planned` one instead of erroring out.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Ralph Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are an autonomous coding agent working on a software project. You apply your skills to build software slices. You only work on one slice at a time.
|
|
4
|
+
|
|
5
|
+
The structure defined in the Project-Skills is relevant.
|
|
6
|
+
|
|
7
|
+
## Context Boundary (READ FIRST — NON-NEGOTIABLE)
|
|
8
|
+
|
|
9
|
+
You work within **exactly ONE context at a time** — the one named in `.build-kit/.slices/current_context.json`.
|
|
10
|
+
|
|
11
|
+
- **ONLY** look for and build slices inside `.build-kit/.slices/<currentContext>/`.
|
|
12
|
+
- **NEVER** read, scan, or build slices from any other context directory, even if it has "Planned" slices, and even if the current context has no work left.
|
|
13
|
+
- A "Planned" slice in a *different* context is **NOT yours to build**. Ignore it completely.
|
|
14
|
+
- If the current context has no "Planned" slice, you are **done for this iteration** — reply `<promise>NO_TASKS</promise>` and stop. Do not go looking elsewhere. The context is only ever changed on the board, never by you.
|
|
15
|
+
|
|
16
|
+
## Your Task
|
|
17
|
+
|
|
18
|
+
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
|
+
1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
|
|
20
|
+
2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
|
|
21
|
+
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
|
+
5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
|
|
23
|
+
**IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
|
|
24
|
+
<promise>NO_TASKS</promise> and stop immediately. Do not work on other slices and do not switch to another context.
|
|
25
|
+
**Claim conflict**: the board rejects the status update if the slice is already in the target status — this is expected: another agent claimed it first, racing you for the same slice. This is NOT an error. Do not stop, do not retry the same slice. Re-read `index.json` (or re-fetch via `load-slice`), pick the next-highest-priority slice still "Planned", and try claiming that one instead. Repeat until a claim succeeds or no "Planned" slice remains, in which case reply `<promise>NO_TASKS</promise>`.
|
|
26
|
+
6. Pick the slice definition from `.build-kit/.slices/<contextName>/<folder>/slice.json` as defined in the prd. Never work on more than one slice per iteration.
|
|
27
|
+
7. A slice can define additional prompts as codegen/backendPrompt. Any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account. If you use the additional prompt, add a line in progress.txt
|
|
28
|
+
7. Determine the slice type and invoke the matching skill as defined in the **Building a Slice** section of `.build-kit/CLAUDE.md`. Do NOT implement manually.
|
|
29
|
+
8. Write a short progress one liner after each step to progress.txt
|
|
30
|
+
9. Analyze and Implement that single slice, making use of the skills in the skills directory plus your previously collected knowledge. Make a TODO list for what needs to be done, and adjust the implementation according to the JSON definition. Carefully inspect events, fields and compare against the implemented slice. JSON is the desired state. ATTENTION: a "planned" task can also be just added specifications. So always look at the slice itself, but also the specifications. If specifications were added in json which are not on code, you need to add them in code.
|
|
31
|
+
10. The slice in the json is always true, the code follows what is defined in the json
|
|
32
|
+
11. Slice is only 'Done' if business logic is implemented as defined in the JSON, APIs are implemented, all scenarios in JSON are implemented in code and it fulfills the slice.json. There must be no specification in json that has no equivalent in code.
|
|
33
|
+
12. Make sure to write the ui-prompt.md as defined if defined in the skill
|
|
34
|
+
13. Run quality checks — `npm run build` (typecheck + build) and `npm run lint`. No test runner is configured in this stack yet; if a slice's specifications demand behavioral tests, flag it via `request-feedback` instead of inventing a test setup ad hoc.
|
|
35
|
+
14. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge (update first)
|
|
36
|
+
15. Update the PRD to set `status: Done` for the completed story in index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
|
|
37
|
+
16. Append your progress to `progress.txt` after each step in the iteration.
|
|
38
|
+
17. Append your new learnings to `.build-kit/AGENTS.md` in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
|
|
39
|
+
18. Finish the iteration.
|
|
40
|
+
|
|
41
|
+
## Escalating Ambiguity
|
|
42
|
+
|
|
43
|
+
**If the slice's requirements are genuinely ambiguous, contradictory, or missing a decision you need
|
|
44
|
+
in order to proceed — do not guess, and do not build anyway.** Invoke `/request-feedback` with the
|
|
45
|
+
specific question; it posts the question as a comment on the slice and marks it `Blocked` on the
|
|
46
|
+
board (overriding the `InProgress` set in step 5), then stop this iteration without finishing the
|
|
47
|
+
build — reply `<promise>DONE</promise>` as if the iteration's work was to raise the question, not to
|
|
48
|
+
implement the slice. This is an escalation path, not a routine step — read the slice.json and the
|
|
49
|
+
matching build skill's own instructions fully first; most slices are fully specified and need none of
|
|
50
|
+
this.
|
|
51
|
+
|
|
52
|
+
## Progress Report Format
|
|
53
|
+
|
|
54
|
+
APPEND to progress.txt (never replace, always append):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
## [Date/Time] - [Slice]
|
|
58
|
+
|
|
59
|
+
- What was implemented
|
|
60
|
+
- Files changed
|
|
61
|
+
- **Learnings for future iterations:**
|
|
62
|
+
- Patterns discovered (e.g., "this codebase uses X for Y")
|
|
63
|
+
- Gotchas encountered (e.g., "don't forget to update Z when changing W")
|
|
64
|
+
- Useful context (e.g., "the evaluation panel is in component X")
|
|
65
|
+
---
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The learnings section is critical - it helps future iterations avoid repeating mistakes and understand the codebase better.
|
|
69
|
+
|
|
70
|
+
## Consolidate Patterns
|
|
71
|
+
|
|
72
|
+
If you discover a **reusable pattern** that future iterations should know, add it to the `## Codebase Patterns` section at the TOP of progress.txt (create it if it doesn't exist).
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
## Codebase Patterns
|
|
76
|
+
- TODO: an example of a your stack-specific reusable pattern once you have one
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Only add patterns that are **general and reusable**, not story-specific details.
|
|
80
|
+
|
|
81
|
+
## Update AGENTS.md Files
|
|
82
|
+
|
|
83
|
+
Before committing, check if any edited files have learnings worth preserving in nearby AGENTS.md files — API patterns/conventions, gotchas, dependencies between files, testing approaches, configuration/environment requirements.
|
|
84
|
+
|
|
85
|
+
**Do NOT add:**
|
|
86
|
+
|
|
87
|
+
- Slice specific implementation details
|
|
88
|
+
- Story-specific implementation details
|
|
89
|
+
- Temporary debugging notes
|
|
90
|
+
- Information already in progress.txt
|
|
91
|
+
- Task-specific learnings
|
|
92
|
+
|
|
93
|
+
Only update AGENTS.md if you have **genuinely reusable knowledge** that would help future work
|
|
94
|
+
|
|
95
|
+
## Quality Requirements
|
|
96
|
+
|
|
97
|
+
- ALL commits must pass this project's quality checks (typecheck/compile, lint, test)
|
|
98
|
+
- TODO: your stack's build command
|
|
99
|
+
- TODO: your stack's test command
|
|
100
|
+
- Do NOT commit broken code
|
|
101
|
+
- Keep changes focused and minimal
|
|
102
|
+
- Follow existing code patterns
|
|
103
|
+
|
|
104
|
+
## Skills
|
|
105
|
+
|
|
106
|
+
Use the provided skills in the skills folder as guidance.
|
|
107
|
+
Update skill definitions if you find an improvement you can make.
|
|
108
|
+
|
|
109
|
+
## Specifications
|
|
110
|
+
|
|
111
|
+
For every specification added to the Slice, you need to implement one executable Specification in Code.
|
|
112
|
+
|
|
113
|
+
A Slice is not complete if specifications are missing or can't be executed.
|
|
114
|
+
|
|
115
|
+
## Stop Condition
|
|
116
|
+
|
|
117
|
+
**After completing ONE slice, always stop — regardless of whether more slices are Planned.** The ralph loop will invoke you again for the next slice. Never chain multiple slices in one iteration.
|
|
118
|
+
|
|
119
|
+
If the slice was completed and committed successfully, reply with:
|
|
120
|
+
<promise>DONE</promise>
|
|
121
|
+
|
|
122
|
+
If no slice has status "Planned" in the current context, reply with:
|
|
123
|
+
<promise>NO_TASKS</promise>
|
|
124
|
+
(Do NOT switch to another context to find work — stop here.)
|
|
125
|
+
|
|
126
|
+
If ALL slices in the current context are Done, reply with:
|
|
127
|
+
<promise>COMPLETE</promise>
|
|
128
|
+
|
|
129
|
+
## Important
|
|
130
|
+
|
|
131
|
+
- If `.build-kit/.eventmodelers/config.json` is absent, skip all platform communication (MCP calls, `update-slice-status`, board sync) and continue working locally.
|
|
132
|
+
- Work on ONE slice per iteration
|
|
133
|
+
- Commit frequently
|
|
134
|
+
- update progress.txt frequently
|
|
135
|
+
- Read the Codebase Patterns section in progress.txt before starting
|
|
136
|
+
|
|
137
|
+
## When an iteration completes
|
|
138
|
+
|
|
139
|
+
Use all the key learnings from the progress.txt and update the `.build-kit/AGENTS.md` file with those learnings.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Agent Task Instructions
|
|
2
|
+
|
|
3
|
+
You are an autonomous agent reacting to slice status change events on an Eventmodelers board.
|
|
4
|
+
|
|
5
|
+
## Your Loop
|
|
6
|
+
|
|
7
|
+
1. Read `AGENT.md` to load accumulated learnings before doing anything else.
|
|
8
|
+
2. Read `.build-kit/tasks.json`.
|
|
9
|
+
3. If `tasks.json` is empty or missing, reply with:
|
|
10
|
+
<promise>IDLE</promise>
|
|
11
|
+
and stop.
|
|
12
|
+
4. Pick the **oldest task** (earliest `createdAt`).
|
|
13
|
+
5. Execute the task — see the Execution section below.
|
|
14
|
+
6. After execution, remove that task from the array and write `.build-kit/tasks.json` back.
|
|
15
|
+
7. Append a progress entry to `progress.txt` (create if missing).
|
|
16
|
+
8. Update `AGENT.md` with any new reusable learnings discovered this iteration.
|
|
17
|
+
9. Reply normally so the next iteration can pick up the next task.
|
|
18
|
+
|
|
19
|
+
## Execution
|
|
20
|
+
|
|
21
|
+
Each task has a single `payload` of type `SliceChangedPayload`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
{
|
|
25
|
+
event: "slice:changed"
|
|
26
|
+
organizationId: string | null
|
|
27
|
+
boardId: string
|
|
28
|
+
sliceId: string ← SLICE_BORDER node UUID
|
|
29
|
+
sliceTitle: string | null
|
|
30
|
+
sliceStatus: string | null ← e.g. "InProgress", "Done", "Blocked"
|
|
31
|
+
timestamp: number
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Step 1 — Load credentials
|
|
36
|
+
|
|
37
|
+
Run `/connect` to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL` from `.eventmodelers/config.json`.
|
|
38
|
+
|
|
39
|
+
### Step 2 — Load the slice
|
|
40
|
+
|
|
41
|
+
Run `/load-slice sliceId=<payload.sliceId>` to fetch full slice details (title, status, raw node record).
|
|
42
|
+
|
|
43
|
+
### Step 3 — Act on the change
|
|
44
|
+
|
|
45
|
+
Inspect the `sliceStatus` in the payload:
|
|
46
|
+
|
|
47
|
+
#### `Planned` — build the slice
|
|
48
|
+
|
|
49
|
+
This is the build trigger. Setting `InProgress` and building are one atomic step:
|
|
50
|
+
|
|
51
|
+
1. Immediately call `/update-slice-status` to set the slice to `InProgress` on the board.
|
|
52
|
+
|
|
53
|
+
**Claim conflict**: if this call reports the slice is already in `InProgress` (or any status other than `Planned`), another agent already claimed it first — this is expected, not an error. Log it in `progress.txt`, drop this task without building, and continue the loop (the next task will naturally cover the next slice). Do not retry.
|
|
54
|
+
|
|
55
|
+
2. Read the slice definition from `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json` (written by `/load-slice`).
|
|
56
|
+
|
|
57
|
+
3. Determine the **slice type** from `sliceType` in the slice.json:
|
|
58
|
+
- **`STATE_CHANGE`** → invoke `/build-state-change`
|
|
59
|
+
- **`STATE_VIEW`** → invoke `/build-state-view`
|
|
60
|
+
- **`AUTOMATION`** → not this stack's job (no UI counterpart) — invoke `/request-feedback`
|
|
61
|
+
rather than building it
|
|
62
|
+
|
|
63
|
+
4. Invoke the matching skill and follow its instructions **completely**. Do NOT implement the slice manually.
|
|
64
|
+
|
|
65
|
+
5. **Verify against slice.json**: cross-check the implementation — every command field, event field, and specification in slice.json must appear in the code. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
66
|
+
|
|
67
|
+
6. Run `npm run build` and `npm run lint` (both must pass clean) — plus the slice's own tests only, if any exist.
|
|
68
|
+
|
|
69
|
+
7. If checks pass, commit all changes with message: `feat: [Slice Name]`.
|
|
70
|
+
|
|
71
|
+
8. Call `/update-slice-status` to set the slice to `Done` on the board — **unless** the matching
|
|
72
|
+
build skill flagged a missing `apiEndpoint` and already marked the slice `Blocked` (see
|
|
73
|
+
"Escalating Ambiguity" below); in that case, skip this step and leave it `Blocked`.
|
|
74
|
+
|
|
75
|
+
#### `InProgress`
|
|
76
|
+
Another agent is already building this slice. Log it and skip — do not build.
|
|
77
|
+
|
|
78
|
+
#### `Done`
|
|
79
|
+
Summarize what was completed and update `progress.txt`.
|
|
80
|
+
|
|
81
|
+
#### `Blocked`
|
|
82
|
+
Log the blocker in `progress.txt`.
|
|
83
|
+
|
|
84
|
+
#### `Review`
|
|
85
|
+
Fetch slice details and prepare a review summary in `progress.txt`.
|
|
86
|
+
|
|
87
|
+
#### Any other status (`Created`, etc.)
|
|
88
|
+
Load the slice and log the state transition in `progress.txt`. No build action.
|
|
89
|
+
|
|
90
|
+
Use the skills available in `.claude/skills/` to interact with the board.
|
|
91
|
+
|
|
92
|
+
## Updating tasks.json
|
|
93
|
+
|
|
94
|
+
After completing a task, remove it from the array and write the updated array back to `.build-kit/tasks.json`. If the array is now empty, write `[]`.
|
|
95
|
+
|
|
96
|
+
## Escalating Ambiguity
|
|
97
|
+
|
|
98
|
+
**If the slice's requirements are genuinely ambiguous, contradictory, or missing a decision you need
|
|
99
|
+
in order to proceed — do not guess, and do not build anyway.** Invoke `/request-feedback` with the
|
|
100
|
+
specific question; it posts the question as a comment on the slice and marks it `Blocked` on the
|
|
101
|
+
board (overriding the `InProgress` set earlier), then stop this iteration without finishing the
|
|
102
|
+
build — reply `<promise>DONE</promise>` as if the iteration's work was to raise the question, not to
|
|
103
|
+
implement the slice. This is an escalation path, not a routine step — read the slice.json and the
|
|
104
|
+
matching build skill's own instructions fully first; most slices are fully specified and need none of
|
|
105
|
+
this.
|
|
106
|
+
|
|
107
|
+
**Exception: a missing `apiEndpoint`** (no backend endpoint/table decided yet) is a partial version of
|
|
108
|
+
this — flag it, but don't fully stop. `build-state-change`/`build-state-view` Step 1 cover the exact
|
|
109
|
+
mechanics: post a comment on the slice and mark it `Blocked` (same as `/request-feedback`), but then
|
|
110
|
+
keep going — build the slice normally against a provisional mock path/table name with meaningful
|
|
111
|
+
sample data. The slice ends this run `Blocked` (step 8 above does not apply for this case — leave it
|
|
112
|
+
`Blocked` instead of setting `Done`), but the code is there, mocked, ready to be pointed at the real
|
|
113
|
+
API once one exists.
|
|
114
|
+
|
|
115
|
+
## Progress Report Format
|
|
116
|
+
|
|
117
|
+
APPEND to `progress.txt` (never replace):
|
|
118
|
+
```
|
|
119
|
+
## [ISO timestamp] — Task [task.id]
|
|
120
|
+
|
|
121
|
+
Slice: [sliceTitle] ([sliceId])
|
|
122
|
+
Status change: [sliceStatus]
|
|
123
|
+
|
|
124
|
+
Action taken:
|
|
125
|
+
- [what was done in response to the slice change]
|
|
126
|
+
|
|
127
|
+
Learnings:
|
|
128
|
+
- [any patterns, gotchas, or reusable knowledge discovered]
|
|
129
|
+
---
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Stop Condition
|
|
133
|
+
|
|
134
|
+
If `.build-kit/tasks.json` is empty (`[]`) or does not exist, reply with:
|
|
135
|
+
<promise>IDLE</promise>
|
|
136
|
+
|
|
137
|
+
## Updating AGENT.md
|
|
138
|
+
|
|
139
|
+
After completing a task, add any **reusable** learnings to `AGENT.md` — patterns, gotchas, API quirks, or skill behaviour that future iterations should know. Only add things that are general and applicable beyond this single task. Do not duplicate what is already there.
|
|
140
|
+
|
|
141
|
+
## Important
|
|
142
|
+
|
|
143
|
+
- Process **one task per iteration**.
|
|
144
|
+
- Read `AGENT.md` first — it contains patterns from previous iterations.
|
|
145
|
+
- Always start with `/connect` if credentials are not yet loaded.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Supabase project this app authenticates against and issues commands as.
|
|
2
|
+
VITE_SUPABASE_URL=
|
|
3
|
+
VITE_SUPABASE_ANON_KEY=
|
|
4
|
+
|
|
5
|
+
# Base URL of the backend that receives command POSTs (empty for same-origin/relative paths).
|
|
6
|
+
VITE_API_BASE=
|
|
7
|
+
|
|
8
|
+
# Set to "mock" to serve every component's samples/sample-<N>.json instead of hitting
|
|
9
|
+
# Supabase/the backend at all — lets any slice component be tested/previewed in isolation.
|
|
10
|
+
VITE_DATA_MODE=
|
|
11
|
+
# Which numbered sample set is active when VITE_DATA_MODE=mock, e.g. "1", "2".
|
|
12
|
+
VITE_MOCK_SAMPLE=1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
|
3
|
+
"plugins": ["react", "typescript", "oxc"],
|
|
4
|
+
"ignorePatterns": [".build-kit/**"],
|
|
5
|
+
"rules": {
|
|
6
|
+
"react/rules-of-hooks": "error",
|
|
7
|
+
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Project
|
|
2
|
+
|
|
3
|
+
A React (Vite + TypeScript) frontend, built slice-by-slice from an eventmodelers board.
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- [Vite](https://vite.dev) + React 19 + TypeScript
|
|
8
|
+
- [oxlint](https://oxc.rs/docs/guide/usage/linter.html) for linting
|
|
9
|
+
- [Supabase](https://supabase.com) for auth — every command POST is authenticated with the
|
|
10
|
+
current Supabase session's JWT (see `src/lib/api.ts` / `src/lib/supabase.ts`)
|
|
11
|
+
|
|
12
|
+
## Scripts
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install # install dependencies
|
|
16
|
+
npm run dev # start the dev server
|
|
17
|
+
npm run build # type-check and build for production
|
|
18
|
+
npm run preview # preview the production build
|
|
19
|
+
npm run lint # lint with oxlint
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Structure
|
|
23
|
+
|
|
24
|
+
- `src/lib/api.ts` / `src/lib/supabase.ts` — the **only** place that knows about base URLs, auth
|
|
25
|
+
headers, and the Supabase client. Slice components never call `fetch` or read the Supabase
|
|
26
|
+
session directly — they call `postCommand(path, body)` and handle only their own request/response
|
|
27
|
+
shape.
|
|
28
|
+
- `src/slices/` — one directory per board slice, generated by the `build-state-change` /
|
|
29
|
+
`build-state-view` skills from `.build-kit/.slices/<context>/<slice>/slice.json`. Do not
|
|
30
|
+
hand-edit slice code without reading the matching `slice.json` first.
|
|
31
|
+
- `.build-kit/` — Ralph's runtime: the agent loop, realtime board subscription, and the
|
|
32
|
+
build skills under `.build-kit/lib/`. See `.build-kit/README.md`.
|
|
33
|
+
- `.claude/skills/` — Claude skills used to build slices (`connect`, `learn-eventmodelers-api`,
|
|
34
|
+
`load-slice`, `build-state-change`, `build-state-view`, `update-slice-status`,
|
|
35
|
+
`request-feedback`) plus `init-style-guide` / `learn-styleguide` for keeping generated UI
|
|
36
|
+
on-brand — run `init-style-guide` once to hand over CSS/design tokens/brand docs.
|
|
37
|
+
|
|
38
|
+
## Environment
|
|
39
|
+
|
|
40
|
+
Copy `.env.example` to `.env` and fill in:
|
|
41
|
+
- `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` — the Supabase project this app authenticates
|
|
42
|
+
against.
|
|
43
|
+
- `VITE_API_BASE` — base URL of the backend that receives command POSTs (leave empty for
|
|
44
|
+
same-origin/relative paths).
|
|
45
|
+
|
|
46
|
+
This stack is UI-only: it builds **state-change** (command → event) and **state-view**
|
|
47
|
+
(read model) slices. Automations/processors are a backend concern and are handled by
|
|
48
|
+
whatever backend stack is installed alongside this one — see the note in
|
|
49
|
+
`.build-kit/CLAUDE.md`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>vite-scaffold</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "oxlint",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@supabase/supabase-js": "^2.115.0",
|
|
14
|
+
"react": "^19.2.8",
|
|
15
|
+
"react-dom": "^19.2.8"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24.13.3",
|
|
19
|
+
"@types/react": "^19.2.18",
|
|
20
|
+
"@types/react-dom": "^19.2.4",
|
|
21
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
22
|
+
"oxlint": "^1.79.0",
|
|
23
|
+
"typescript": "~6.0.2",
|
|
24
|
+
"vite": "^8.2.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|