@eventmodelers/cli 1.0.74 → 1.0.76
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 +7 -5
- package/cli.js +80 -28
- package/package.json +1 -1
- package/shared/build-kit/README.md +64 -9
- package/shared/build-kit/lib/local-ai-agent.js +281 -0
- package/shared/build-kit/lib/ralph.js +1 -1
- package/shared/build-kit/ralph-exec.js +92 -0
- package/shared/build-kit/ralph-local-ai.js +40 -0
- package/shared/build-kit/realtime-agent.js +1 -1
- package/shared/skills/learn-eventmodelers-api/SKILL.md +93 -43
- package/shared/skills/update-slice-status/SKILL.md +8 -0
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/bridge/templates/bridge/lib/AGENT.md +1 -1
- package/stacks/bridge/templates/bridge/ralph-local-ai.js +43 -0
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +11 -1
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +8 -3
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/react/templates/build-kit/README.md +64 -9
- package/stacks/react/templates/build-kit/lib/prompt.md +1 -1
- package/stacks/react/templates/build-kit/lib/ralph.js +1 -1
- package/stacks/react/templates/build-kit/ralph-local-ai.js +40 -0
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
- package/shared/build-kit/lib/ollama-agent.js +0 -147
- package/shared/build-kit/ralph-ollama.js +0 -40
- package/stacks/bridge/templates/bridge/ralph-ollama.js +0 -43
- package/stacks/react/templates/build-kit/ralph-ollama.js +0 -40
|
@@ -27,35 +27,35 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
|
|
|
27
27
|
| `search_board_events` | `boardId`, `name` | Search events by node name | §1 `GET .../events/search` |
|
|
28
28
|
| `submit_node_events` | `boardId`, `events[]`, `autoConnect?`, `compact?` | Create/update nodes (raw `NodeChangeEvent`/edge events). `autoConnect: false` places freshly-created nodes without wiring them to their own/previous-column neighbors (avoids a stray nearest-left edge); `compact: true` returns `{persisted: <count>}` instead of the per-node hash map | §3 `POST .../nodes/events` |
|
|
29
29
|
| `delete_node` | `boardId`, `nodeId` | Delete a node. Deleting a chapter (timeline) cascades — every node placed in one of its cells, plus any node parented to it (e.g. SLICE_BORDER), is deleted too, along with all their edges | (via `node:deleted` event, §3) |
|
|
30
|
-
| `create_drawing` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... | Freehand canvas annotation (path/rect/text) — never placed in a cell | — (
|
|
30
|
+
| `create_drawing` / `create_drawings` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... (plural: `drawings[]`) | Freehand canvas annotation (path/rect/text/sticky) — never placed in a cell. Use the plural form whenever an annotation is more than one stroke (a loop plus its arrows and label is one annotation, not three calls) | — (REST `POST .../drawing/draw` accepts a single drawing or an array) |
|
|
31
31
|
| `find_nodes_in_drawing` | `boardId`, `drawingId` | Nodes fully contained inside a drawing's bounding box | — (no REST equivalent; MCP-only) |
|
|
32
|
-
| `create_chapter` | `boardId`, `x?`, `y
|
|
32
|
+
| `create_chapter` | `boardId`, `x?`, `y?`, `title?`, `columns?`, `lanes?: [{type, label?, height?}]` | Create a timeline. Omitting `x`/`y` auto-stacks it below the lowest existing chapter (by its *actual current* row-height total, not the height it was created with — safe even after `add_lane` growth), plus a fixed margin. Pass `title` to name it, `columns` for a known column count, and `lanes` to create named lanes (or several of one type, e.g. one swimlane per context) — none of the three needs a follow-up call, and the lanes are sorted into the required order automatically. The response carries every `columnId` and lane id, so the chapter needn't be read back before placing into it | §2 `POST .../chapters` |
|
|
33
33
|
| `get_chapter_bounds` | `boardId` | Absolute canvas bounding box `{id, title, x, y, width, height}` of every chapter on the board — width/height derived from each chapter's current row/column layout, not a guessed default. Use before picking explicit `x`/`y` for `create_chapter` (e.g. placing below the chapter with the largest `y + height`) to avoid overlapping one that grew since it was created | §2 `GET .../chapters/bounds` |
|
|
34
34
|
| `add_column` | `boardId`, `timelineId`, `index?`, `beforeNodeId?`, `afterNodeId?`, `count?` | Add one or more columns in one call. `count` inserts that many contiguously starting at the insertion point (default 1). Position with at most one of `index` (0-based), `beforeNodeId`, or `afterNodeId` (resolves the index from where that already-placed node currently sits) — omit all three to append | §2 `POST .../timelines/:id/columns` |
|
|
35
35
|
| `delete_column` | `boardId`, `timelineId`, `columnId` | Delete a column | §2 `DELETE .../columns/:columnId` |
|
|
36
|
-
| `add_lane` | `boardId`, `timelineId`, `type`, `label?`, `index?` | Add a lane/row | §2 `POST .../timelines/:id/lanes` |
|
|
36
|
+
| `add_lane` / `add_lanes` | `boardId`, `timelineId`, `type`, `label?`, `index?`, `height?` (plural: `lanes[]`) | Add a lane/row. Use the plural form for more than one; for a chapter that doesn't exist yet, pass `lanes` to `create_chapter` instead — that needs no lane call at all | §2 `POST .../timelines/:id/lanes` (accepts an array too) |
|
|
37
37
|
| `remove_lane` | `boardId`, `timelineId`, `rowId` | Remove a lane | — (extends §2; no direct REST route) |
|
|
38
38
|
| `move_node_in_timeline` | `boardId`, `timelineId`, `movedNodeId`, `toCellId` | Move a placed node to another cell — its previous cell is automatically cleared | — (MCP-only convenience) |
|
|
39
39
|
| `move_timeline_structure` | `boardId`, `timelineId`, `kind` (`'column'\|'lane'`), `id`, `toIndex` | Reorder a column or lane (row) — `kind` picks which `id` refers to | — (MCP-only convenience) |
|
|
40
40
|
| `move_timeline_position` | `boardId`, `timelineId`, `x`, `y` | Move a chapter node on canvas | — (MCP-only convenience) |
|
|
41
41
|
| `drop_node_to_cell` | `boardId`, `timelineId`, `cellId`, `nodeId`, `nodeType` | Place an existing node into a cell — if it was already placed elsewhere on this timeline, that cell is automatically cleared | §2 `POST .../cells/:cellId/drop` |
|
|
42
42
|
| `clear_cell` | `boardId`, `timelineId`, `cellId` | Unassign the node from a cell without deleting it — the cell becomes empty and the node survives (unplaced); no-op if already empty. Use `delete_node` to remove the node entirely | — (MCP-only convenience) |
|
|
43
|
-
| `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?}`) | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane | §5 `POST .../slices` |
|
|
44
|
-
| `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column | §5 `POST .../slice-definitions` |
|
|
45
|
-
| `place_element` | `boardId`, `timelineId`, `elementType`, `title`, `columnIndex?`, `compact?`, `autoConnect?` | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT. `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
|
|
43
|
+
| `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?, fields?}`), `status?` | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane. `fields` writes that node's attributes in the same call, and `status` gives the SLICE_BORDER its `sliceStatus` on creation — neither needs a follow-up write | §5 `POST .../slices` |
|
|
44
|
+
| `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `status?`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column. `status` sets its `sliceStatus` straight away instead of a follow-up `update_slice_status` | §5 `POST .../slice-definitions` |
|
|
45
|
+
| `place_element` / `place_elements` | `boardId`, `timelineId`, `elementType`, `title`, `fields?`, `lane?`, `columnIndex?`, `compact?`, `autoConnect?` (plural: `elements[]`) | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT. `fields` writes the element's attributes in the same call — don't follow a placement with a `submit_node_events` just to set them. The plural form places a whole slice's or column run's worth in one call, applied in order so each entry sees the columns the previous one added. `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
|
|
46
46
|
| `list_slices` | `boardId` | List slices (id, title, status) | §8 `GET .../slicedata/slices` |
|
|
47
|
-
| `update_slice_status` | `boardId`, `
|
|
47
|
+
| `update_slice_status` | `boardId`, `newStatus`, plus exactly one of `sliceId` / `sliceTitle` / `columnId` | Change a SLICE_BORDER's `sliceStatus`. With a title or column id there is no need to call `list_slices` first; an ambiguous title comes back with its candidates. A slice being created takes its status from `create_slice`/`create_slice_definition` instead | — (via `node:changed` event, §3) |
|
|
48
48
|
| `get_slice_data` | `boardId`, `contextName?`, `contextId?`, `sliceId?` | Full element graph for slices in a context | §8 `GET /slicedata` |
|
|
49
49
|
| `get_spec_info` | `boardId`, `timelineId`, `elementTypes?` | EVENT/COMMAND/READMODEL nodes valid in GWT steps. Pass `elementTypes` (subset of `EVENT`/`COMMAND`/`READMODEL`) to avoid pulling the full element list when only one or two types are needed — filtered server-side, not just after a full fetch | §6 `GET .../spec-info` |
|
|
50
50
|
| `get_board_outline` | `boardId`, `chapterId` | One chapter's structure, compact: per-column node lists (`{id, type, title, lane}`) + a flat edge list, no HTML pages / field bodies / meta. The cheap "what is where and how is it wired" read — prefer over `get_nodes` (no projection) for orientation checks | — (MCP-only convenience) |
|
|
51
51
|
| `get_connected_nodes` | `boardId`, `nodeId`, `chapterId?`, `direction?` (`inbound`/`outbound`/`both`), `depth?`, `types?`, `includeFields?` | Neighbours of **one** node — what feeds it and what it feeds. Answers from a single anchor, unlike `get_attribute_chain` (which needs both ends of the chain as cell names up front). `depth` follows a whole chain; `types` filters the result only, never the traversal. Each neighbour carries `via`: `"edge"` for a real connection, `"layout"` when the node has none in that direction and the neighbour was inferred from the grid using auto-connect's own window (own column + adjacent one, forward-only pairs). Real edges always win. The `layout` fallback is what makes hand-built/imported chapters — which routinely carry **zero** edges — readable instead of falsely empty | — (MCP-only convenience) |
|
|
52
52
|
| `validate_model` | `boardId`, `chapterId`, `checks?[]` | Server-side Event Modeling structural checklist over one chapter — compact `findings` only. Checks: unplaced nodes, backward arrows (with the todo-list `EVENT→READMODEL` exception), zero/multi-issuer commands, sourceless read models, two-screens-in-a-column, missing scenarios. Replaces the manual per-type `get_nodes` + `get_node projection=edges` validation pass | — (MCP-only convenience) |
|
|
53
|
-
| `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node. `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
|
|
53
|
+
| `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node — created automatically, and a scenario `id` is generated when omitted. A given/when/then step may be addressed by `{title, type}` instead of a node id, resolved against that timeline, so no `get_spec_info` call is needed first (an ambiguous title is reported with its candidates). `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
|
|
54
54
|
| `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node. Use whenever `eventmodeling-elaborating-scenarios`'s GWT-vs-storyline decision rule calls for one (e.g. a todo list's open→close lifecycle) — not only when a user explicitly names "storyline"; that skill's own per-read-model judgment is the trigger, this catalog entry isn't a stricter gate on top of it. `compact: true` suppresses the full storyline echo | §6 `POST .../storylines` |
|
|
55
55
|
| `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) |
|
|
56
56
|
| `auto_connect_node` | `boardId`, `nodeId` | Re-run auto-connect for a node | §3 `POST .../nodes/:nodeId/auto-connect` |
|
|
57
|
-
| `link_element` | `boardId`, `nodeId`, `targetNodeId` |
|
|
58
|
-
| `add_comment` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'`), `author?` | Add a comment —
|
|
57
|
+
| `link_element` | `boardId`, `nodeId`, plus either `targetNodeId` or `timelineId` (+ `columnIndex?`, `lane?`) | Turn a node into a linked copy of `nodeId` — it receives a full copy of that node's meta plus `meta.linkedTo`. Name an existing `targetNodeId`, or pass `timelineId` to have the copy placed and linked in this one call (inheriting the original's type and title), which is what a translation or automation chain wants | §3 `POST .../nodes/:nodeId/link` |
|
|
58
|
+
| `add_comment` / `add_comments` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'\|'QUESTION'`), `author?` (plural: `comments[]`, each with its own `nodeId`) | Add a comment — `QUESTION` is the type for a gap/edge case raised during review. Use the plural form for a review that has a question per element: all of them go in one call | §1 `POST .../boards/:boardId/comments` (batch) |
|
|
59
59
|
| `update_comment` | `boardId`, `nodeId`, `commentId`, `action` (`'resolve'\|'delete'`) | Resolve or delete a comment | — (via comment events) |
|
|
60
60
|
| `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` |
|
|
61
61
|
| `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` |
|
|
@@ -209,6 +209,24 @@ Get all comments for a node.
|
|
|
209
209
|
|
|
210
210
|
---
|
|
211
211
|
|
|
212
|
+
### POST `/api/org/:orgId/boards/:boardId/nodes/:nodeId/comments`
|
|
213
|
+
Add one comment to one node.
|
|
214
|
+
|
|
215
|
+
**Request body**: `{ text: string, type?: 'COMMENT' | 'TASK' | 'QUESTION', author?: string }`
|
|
216
|
+
**Response**: `201` — `{ id }`
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### POST `/api/org/:orgId/boards/:boardId/comments`
|
|
221
|
+
Post several comments, on any nodes of one board, in a single request — the batch form. Use it for a review that raises a question per element (`/wdyt`) instead of one request per comment.
|
|
222
|
+
|
|
223
|
+
**Request body**: `Array<{ nodeId: string, text: string, type?: 'COMMENT' | 'TASK' | 'QUESTION', author?: string }>`
|
|
224
|
+
**Response**: `201` — `{ results: Array<{ nodeId, id } | { nodeId, error }> }`, in request order
|
|
225
|
+
|
|
226
|
+
Entries are independent: one naming a node that doesn't exist is reported in `results` while the rest are still posted.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
212
230
|
### POST `/api/org/:orgId/boards/:boardId/bucket`
|
|
213
231
|
Create a Supabase storage bucket for the board.
|
|
214
232
|
|
|
@@ -225,8 +243,19 @@ A "chapter" is a timeline — the same entity, referenced as `chapterId` in node
|
|
|
225
243
|
### POST `/api/org/:orgId/boards/:boardId/chapters`
|
|
226
244
|
Create a chapter node.
|
|
227
245
|
|
|
228
|
-
**Request body**:
|
|
229
|
-
|
|
246
|
+
**Request body**:
|
|
247
|
+
```typescript
|
|
248
|
+
{
|
|
249
|
+
position?: { x: number, y: number }
|
|
250
|
+
title?: string // names the chapter on creation — no follow-up rename
|
|
251
|
+
columns?: number // initial column count, default 3
|
|
252
|
+
lanes?: Array<{ type: 'actor'|'interaction'|'swimlane'|'spec'|'feedback'|'table', label?: string, height?: number }>
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Response**: `200` — `{ id, eventId, columnIds: string[], lanes: Array<{id, type, label}> }`
|
|
257
|
+
|
|
258
|
+
`lanes` replaces the default Actor/Interaction/Swimlane/Spec set — this is how a chapter gets lanes named after what they hold, or several lanes of one type (one swimlane per context), without an `add_lane` call per lane. They are sorted into the required order (actor → interaction → swimlane → spec → feedback/table) automatically, keeping the given order within a type, so no ordering error is possible. Every column id and lane id comes back in the response — don't re-read the chapter just to place something into it.
|
|
230
259
|
|
|
231
260
|
Omitting `position` auto-stacks the new chapter below the lowest existing chapter on the board, using each existing chapter's *actual current* row-height total (not the height it was created with) plus a fixed margin — so a chapter that grew via `add_lane`/`add_column` after another was stacked below it won't get overlapped by yet another auto-stacked chapter.
|
|
232
261
|
|
|
@@ -258,18 +287,22 @@ Delete a column from a timeline. Removes the column and all its cells. Cannot de
|
|
|
258
287
|
---
|
|
259
288
|
|
|
260
289
|
### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes`
|
|
261
|
-
Add
|
|
290
|
+
Add one or more lanes (rows) to a timeline.
|
|
262
291
|
|
|
263
292
|
**Request body**:
|
|
264
293
|
```typescript
|
|
265
294
|
{
|
|
266
|
-
type: 'actor' | 'interaction' | 'swimlane' | 'spec' | 'feedback'
|
|
295
|
+
type: 'actor' | 'interaction' | 'swimlane' | 'spec' | 'feedback' | 'table'
|
|
267
296
|
label?: string
|
|
268
297
|
index?: number
|
|
269
298
|
height?: number
|
|
270
299
|
}
|
|
300
|
+
// or, batch form — every lane added in one request, applied in order:
|
|
301
|
+
[ { type: 'interaction', label: 'Interaction' }, { type: 'swimlane', label: 'Ordering' } ]
|
|
271
302
|
```
|
|
272
|
-
**Response**: `200` — lane data
|
|
303
|
+
**Response**: `200` — lane data, or `{ lanes: [...] }` for the batch form
|
|
304
|
+
|
|
305
|
+
For a chapter that doesn't exist yet, pass `lanes` to `POST .../chapters` instead — that needs no lane call at all.
|
|
273
306
|
|
|
274
307
|
---
|
|
275
308
|
|
|
@@ -434,16 +467,27 @@ Create a single type-checked directed edge between two existing nodes — the RE
|
|
|
434
467
|
---
|
|
435
468
|
|
|
436
469
|
### POST `/api/org/:orgId/boards/:boardId/nodes/:nodeId/link`
|
|
437
|
-
|
|
470
|
+
Turn a node into a linked copy of `:nodeId` — the REST fallback for `link_element`. The copy receives a full copy of `:nodeId`'s meta (a replacement, not a merge) plus `meta.linkedTo`. COMMAND/EVENT/READMODEL only; `:nodeId` must not itself already be a linked copy.
|
|
438
471
|
|
|
439
|
-
**Request body
|
|
472
|
+
**Request body** — exactly one of the two:
|
|
440
473
|
```typescript
|
|
441
474
|
{
|
|
442
475
|
targetNodeId: string // existing same-type node to convert into a linked copy
|
|
443
476
|
}
|
|
477
|
+
// or: create the copy in this same request
|
|
478
|
+
{
|
|
479
|
+
place: {
|
|
480
|
+
timelineId: string // chapter to place the copy on
|
|
481
|
+
columnIndex?: number // columns are added to reach it
|
|
482
|
+
lane?: string // row id or label, when the chapter has several lanes of that type
|
|
483
|
+
autoConnect?: boolean // default true
|
|
484
|
+
}
|
|
485
|
+
}
|
|
444
486
|
```
|
|
445
487
|
|
|
446
|
-
|
|
488
|
+
With `place` the copy inherits the original's element type and title, so a translation or automation chain no longer needs a `node:created` call before this one.
|
|
489
|
+
|
|
490
|
+
**Response**: `200` — `{ nodeId, linkedTo, type }`, plus `placed: { nodeId, cellName, columnIndex }` when `place` was used · `400` — neither or both of `targetNodeId`/`place`, type mismatch, self-link, unsupported element type, or the original is itself a linked copy · `404` — the original or `targetNodeId` doesn't exist
|
|
447
491
|
|
|
448
492
|
---
|
|
449
493
|
|
|
@@ -510,7 +554,7 @@ Create a SCREEN node from a sketch description.
|
|
|
510
554
|
**File**: `src/slices/change/api-.slices/routes.ts`
|
|
511
555
|
|
|
512
556
|
### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/slices`
|
|
513
|
-
Create a complete slice (1 column +
|
|
557
|
+
Create a complete slice (1 column + its nodes automatically placed).
|
|
514
558
|
|
|
515
559
|
**Request body**:
|
|
516
560
|
```typescript
|
|
@@ -518,18 +562,23 @@ Create a complete slice (1 column + 3 nodes automatically placed).
|
|
|
518
562
|
type: 'state-change' | 'state-view' | 'automation'
|
|
519
563
|
index?: number
|
|
520
564
|
nodes?: {
|
|
521
|
-
actor?: Partial<NodeData> & { rowId?: string }
|
|
522
|
-
interaction?: Partial<NodeData> & { rowId?: string }
|
|
523
|
-
swimlane?: Partial<NodeData> & { rowId?: string }
|
|
565
|
+
actor?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
|
|
566
|
+
interaction?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
|
|
567
|
+
swimlane?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
|
|
524
568
|
}
|
|
569
|
+
status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
525
570
|
}
|
|
526
571
|
```
|
|
527
572
|
|
|
573
|
+
`fields` writes that node's attributes in the same call, and `status` gives the SLICE_BORDER its `sliceStatus` on creation — neither needs a follow-up write.
|
|
574
|
+
|
|
528
575
|
**Slice node mapping**:
|
|
529
576
|
- `state-change` → HTML_SCREEN (actor) + COMMAND (interaction) + EVENT (swimlane)
|
|
530
|
-
- `state-view` → HTML_SCREEN (actor) + READMODEL (interaction) + EVENT (swimlane)
|
|
577
|
+
- `state-view` → HTML_SCREEN (actor) + READMODEL (interaction) + EVENT (swimlane, **only when `nodes.swimlane` is passed**)
|
|
531
578
|
- `automation` → AUTOMATION (actor) + COMMAND (interaction) + EVENT (swimlane)
|
|
532
579
|
|
|
580
|
+
A `state-view`'s read model is normally fed by an event that already exists in an earlier column, so its event is opt-in: pass `nodes.swimlane` (an empty object is enough) to have one created, omit it to mean "an existing event feeds this". When omitted, the response has no `nodes.swimlane` and auto-connect wires the read model to the type-compatible event in the previous column — previously an untitled EVENT was placed in the slice's swimlane slot, which then had to be removed with `delete_node`. A `state-change`/`automation` event is the slice's own output and is always placed. The endpoint never creates a lane: a chapter with no swimlane lane yields a slice without an event rather than an error.
|
|
581
|
+
|
|
533
582
|
Each chapter has exactly one actor/interaction/swimlane lane by default, but a chapter can have several lanes of the same type (e.g. multiple actor lanes). Without a `rowId`, the node is always placed in the **first** lane of the matching type — pass `nodes.<actor|interaction|swimlane>.rowId` (a row id from the chapter's `timelineData.rows`) to target a specific lane instead. An invalid `rowId` (not found, or found but the wrong lane type) is a `400 ROW_NOT_FOUND`/`ROW_TYPE_MISMATCH` error.
|
|
534
583
|
|
|
535
584
|
The actor HTML_SCREEN is created as a **stub** — a single visibly-placeholder page ("Untitled screen — design pending") unless `nodes.actor.pages` is passed explicitly. Whoever calls this (the `add-next-slice` skill — the one that creates a brand-new slice from scratch, as opposed to `eventmodeling-slicing-event-models`, which only makes existing elements explicit) is responsible for immediately replacing that stub via the `html-screen` skill — including gathering the board's existing screens first so the new one matches their established style, since `html-screen` itself has no visibility into other screens.
|
|
@@ -544,12 +593,13 @@ Create a standalone SLICE_BORDER node spanning an **existing** column. Unlike th
|
|
|
544
593
|
{
|
|
545
594
|
columnId: string // id of an existing column on this timeline
|
|
546
595
|
title: string // slice title — always taken from this field, never derived
|
|
596
|
+
status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
|
|
547
597
|
data?: Record<string, unknown> // optional node.data payload
|
|
548
598
|
meta?: Record<string, unknown> // optional extra meta fields (type, colId, title are always set explicitly and cannot be overridden here)
|
|
549
599
|
}
|
|
550
600
|
```
|
|
551
601
|
|
|
552
|
-
**Response**: `200` — `{ nodeId, timelineId, columnId, title }`
|
|
602
|
+
**Response**: `200` — `{ nodeId, timelineId, columnId, title, status? }` — `status` sets `sliceStatus` straight away, instead of a follow-up `update_slice_status`
|
|
553
603
|
**Errors**: `400` missing `columnId`/`title` or column not found · `404` timeline not found
|
|
554
604
|
|
|
555
605
|
---
|
|
@@ -558,47 +608,47 @@ Create a standalone SLICE_BORDER node spanning an **existing** column. Unlike th
|
|
|
558
608
|
|
|
559
609
|
**File**: `src/slices/change/api-specs/routes.ts`
|
|
560
610
|
|
|
561
|
-
### POST `/api/org/:orgId/boards/:boardId/
|
|
562
|
-
Append
|
|
611
|
+
### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId/scenarios`
|
|
612
|
+
Append Given-When-Then scenario(s) to a column's spec node — the node is created automatically if the spec cell is empty. Accepts a single scenario object or an array.
|
|
563
613
|
|
|
564
614
|
**Request body**:
|
|
565
615
|
```typescript
|
|
566
616
|
{
|
|
567
|
-
id
|
|
617
|
+
id?: string // generated when omitted
|
|
568
618
|
title: string
|
|
569
619
|
vertical?: boolean
|
|
570
620
|
examples?: unknown[]
|
|
571
|
-
given
|
|
572
|
-
when
|
|
573
|
-
then
|
|
621
|
+
given?: SpecStep[] // EVENTs from this timeline
|
|
622
|
+
when?: SpecStep[] // at most one COMMAND, or one inline QUERY object; empty if then has a READMODEL
|
|
623
|
+
then?: SpecStep[] // EVENTs only OR exactly one READMODEL (not mixed)
|
|
624
|
+
expectError?: boolean, errorDescription?: string // error case: leave `then` empty
|
|
574
625
|
}
|
|
626
|
+
|
|
627
|
+
// A step is addressed either way:
|
|
628
|
+
{ id: 'node-uuid' } // a board node
|
|
629
|
+
{ title: 'OrderPlaced', type: 'EVENT' } // resolved against this timeline's own elements
|
|
575
630
|
```
|
|
576
631
|
|
|
632
|
+
A step given as `{title, type}` is resolved server-side, so the names you already know need **no `get_spec_info`/`get_nodes` call first**. A title matching more than one element is rejected (`SCENARIO_ITEM_AMBIGUOUS`) with the candidates rather than guessed at, and resolution runs before anything is written — a bad name leaves no empty spec node behind.
|
|
633
|
+
|
|
577
634
|
**Validation rules**:
|
|
578
635
|
- `given`: only EVENTs from same timeline
|
|
579
|
-
- `when`: max one COMMAND; must be empty when `then` contains a READMODEL
|
|
636
|
+
- `when`: max one COMMAND; must be empty when `then` contains a READMODEL (use an inline QUERY item for a state-view)
|
|
580
637
|
- `then`: all EVENTs OR exactly one READMODEL — never mixed
|
|
581
638
|
- All referenced nodes must belong to the same chapter/timeline
|
|
582
639
|
|
|
583
640
|
**Response**:
|
|
584
|
-
- `201` — `{
|
|
641
|
+
- `201` — `{ specNodeId, scenarios, added, isNewNode: boolean }`
|
|
585
642
|
- `400` — validation error
|
|
586
|
-
- `404` —
|
|
643
|
+
- `404` — timeline, column, or a referenced node not found
|
|
587
644
|
- `409` — duplicate scenario title
|
|
588
645
|
|
|
589
646
|
---
|
|
590
647
|
|
|
591
|
-
### GET `/api/org/:orgId/boards/:boardId/
|
|
592
|
-
Get
|
|
593
|
-
|
|
594
|
-
**Response**: `200` — `{ chapterId: string, elements: ElementRecord[] }`
|
|
595
|
-
|
|
596
|
-
---
|
|
597
|
-
|
|
598
|
-
### GET `/api/org/:orgId/boards/:boardId/contexts/:contextName/slices/:sliceName/spec-info`
|
|
599
|
-
Get valid elements for a specific slice.
|
|
648
|
+
### GET `/api/org/:orgId/boards/:boardId/timelines/:timelineId/spec-info`
|
|
649
|
+
Get the elements a scenario's given/when/then steps may legally reference (EVENT, COMMAND, READMODEL of that timeline). Only needed when you want the ids themselves — a scenario step can name its element by title instead.
|
|
600
650
|
|
|
601
|
-
**Response**: `200` — `{
|
|
651
|
+
**Response**: `200` — `{ timelineId: string, elements: ElementRecord[] }`
|
|
602
652
|
|
|
603
653
|
---
|
|
604
654
|
|
|
@@ -71,6 +71,14 @@ Prefer the MCP tool — it does the same `node:changed`/`sliceStatus` update in
|
|
|
71
71
|
mcp__eventmodelers__update_slice_status { "boardId": "<BOARD_ID>", "sliceId": "<SLICE_NODE_ID>", "newStatus": "<newStatus>" }
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
The tool also resolves the slice itself — pass `sliceTitle` (or `columnId`) instead of `sliceId` and the listing step above can be skipped entirely; an ambiguous title comes back with its candidates instead of a guess:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
mcp__eventmodelers__update_slice_status { "boardId": "<BOARD_ID>", "sliceTitle": "Place Order", "newStatus": "<newStatus>" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A slice that is being *created* takes its status straight from `create_slice`/`create_slice_definition` (`status`) — don't create it and then call this.
|
|
81
|
+
|
|
74
82
|
**Fallback (no MCP)** — send a `node:changed` event to update the `sliceStatus` field in the SLICE_BORDER node's meta directly:
|
|
75
83
|
|
|
76
84
|
```bash
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|
|
@@ -38,7 +38,7 @@ timestamp unix ms when the change was emitted
|
|
|
38
38
|
|
|
39
39
|
## Executors
|
|
40
40
|
|
|
41
|
-
- Claude (`ralph-claude.js`, this prompt) is the default. `bridge --
|
|
41
|
+
- Claude (`ralph-claude.js`, this prompt) is the default. `bridge --local-ai`
|
|
42
42
|
swaps in a local model instead — same prompt, different executor.
|
|
43
43
|
- `bridge --hook "<command>"` (or a `hookCommand` persisted in
|
|
44
44
|
`bridge.json`) bypasses this prompt entirely: `ralph-hook.js` runs an
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bridge loop using a local AI model as the executor. Same caveats as
|
|
3
|
+
// build-kit's ralph-local-ai.js — lib/local-ai-agent.js is shared as-is (see
|
|
4
|
+
// useShared in cli.js), unmodified for bridge.
|
|
5
|
+
// Backend is selected by dialect, not by a separate runner: Ollama (native
|
|
6
|
+
// /api/chat) or any OpenAI-compatible server (vLLM, LM Studio, llama.cpp, TGI).
|
|
7
|
+
//
|
|
8
|
+
// Usage: node ralph-local-ai.js [project_dir]
|
|
9
|
+
// LOCAL_AI_TARGET=ollama node ralph-local-ai.js # run `ollama serve` first
|
|
10
|
+
// LOCAL_AI_TARGET=vllm node ralph-local-ai.js
|
|
11
|
+
// LOCAL_AI_URL=http://gpu-box:8000/v1 LOCAL_AI_MODEL=Qwen/Qwen3-8B node ralph-local-ai.js
|
|
12
|
+
import { startRalph } from './lib/ralph.js';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { dirname, join, resolve } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const kitDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function runLocalAi() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const proc = spawn('node', [join(kitDir, 'lib', 'local-ai-agent.js')], {
|
|
24
|
+
cwd: projectDir,
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`local-ai-agent exited ${code}`))));
|
|
29
|
+
proc.on('error', reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
startRalph({
|
|
34
|
+
kitDir,
|
|
35
|
+
projectDir,
|
|
36
|
+
onTask: runLocalAi,
|
|
37
|
+
// onPlannedSlice omitted — local-ai-agent manages its own task queue
|
|
38
|
+
agentType: 'BRIDGE',
|
|
39
|
+
queueAllStatuses: true,
|
|
40
|
+
}).catch((err) => {
|
|
41
|
+
console.error('[ralph] Fatal:', err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|
|
@@ -48,7 +48,17 @@ mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "nodeId": "$NODE_ID",
|
|
|
48
48
|
|
|
49
49
|
**Fallback (no MCP):** see `references/api-fallback.md` — "Action: place".
|
|
50
50
|
|
|
51
|
-
**Batching (when called in bulk, e.g. from `wdyt`):** send
|
|
51
|
+
**Batching (when called in bulk, e.g. from `wdyt`):** send them all in one request instead of one per comment — each entry names its own node:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/comments" \
|
|
55
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
56
|
+
-H "Content-Type: application/json" \
|
|
57
|
+
-d '[{"nodeId":"<id>","text":"<text>","type":"QUESTION","author":"wdyt"},
|
|
58
|
+
{"nodeId":"<id2>","text":"<text2>","type":"QUESTION","author":"wdyt"}]'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Response: `201 {"results":[{"nodeId":"<id>","id":"<commentId>"}, …]}` in request order — an entry whose node doesn't exist reports `error` there without dropping the rest. Over MCP this is the `add_comments` tool.
|
|
52
62
|
|
|
53
63
|
**Report:**
|
|
54
64
|
```
|
|
@@ -46,10 +46,13 @@ mcp__eventmodelers__place_element {
|
|
|
46
46
|
"timelineId": "<TIMELINE_ID>",
|
|
47
47
|
"elementType": "<COMMAND|READMODEL|EVENT>",
|
|
48
48
|
"title": "<title>",
|
|
49
|
-
"columnIndex": <position, if given
|
|
49
|
+
"columnIndex": <position, if given>,
|
|
50
|
+
"fields": [{ "name": "orderId", "type": "String", "example": "ord-1" }]
|
|
50
51
|
}
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
Pass `fields` whenever the element's attributes are already known — they are written by this same call, so don't follow a placement with a `submit_node_events` just to set them. Placing **more than one** element is `place_elements` with an `elements` array (same per-entry options), applied in order so each entry sees the columns the previous one added — one call for a whole slice's or column run's worth instead of one per element.
|
|
55
|
+
|
|
53
56
|
This tool finds or creates an empty cell in the correct lane and places the node in one call — it collapses the "resolve timeline → fetch columns → determine lane → check occupancy → create node" sequence (Steps 2–3, 4, 6, 7b below) into a single round trip. A `columnIndex` past the timeline's current column count is handled automatically (columns are added to reach it) — no need to pre-check the column count or catch an out-of-range error yourself. If `timelineId` is unknown, resolve it first via Step 2's MCP call. Pass `compact: true` for a smaller `{nodeId, cellName, columnIndex}` response (plus `connectedCount` if auto-connect wired an edge) when you don't need the full `lane`/`elementType`/`title`/`autoConnected` detail back. Go straight to Step 8 once it returns.
|
|
54
57
|
|
|
55
58
|
**This does not cover**: `SCREEN`/`AUTOMATION`/`SCENARIO` (see their dedicated steps below), the `"after <title>"` position form, or an explicit `cellName` fast path (Step 1) — `place_element` has no way to express either. For those cases, or when MCP isn't connected, fall through to the manual steps below.
|
|
@@ -246,12 +249,14 @@ If no matching row is found, stop and report the error — the timeline may be m
|
|
|
246
249
|
|
|
247
250
|
**Connections only ever pair nodes on the same timeline** — a node in Chapter A can never be wired directly to a node in Chapter B, even for an otherwise-valid type pair (e.g. `EVENT → READMODEL`). If the element you're placing needs to connect to something that lives on a *different* timeline, do not place it and then attempt `set_connection`/auto-connect across timelines — it will fail.
|
|
248
251
|
|
|
249
|
-
Instead, create a **linked copy
|
|
252
|
+
Instead, create a **linked copy**. `link_element` places it for you — pass the timeline (and optionally the column/lane) instead of an already-placed `targetNodeId`, and it creates the copy with the origin's type and title and links it in the same call:
|
|
250
253
|
|
|
251
254
|
```
|
|
252
|
-
mcp__eventmodelers__link_element { "boardId": "<BOARD_ID>", "nodeId": "<origin-node-id>", "
|
|
255
|
+
mcp__eventmodelers__link_element { "boardId": "<BOARD_ID>", "nodeId": "<origin-node-id>", "timelineId": "<TIMELINE_ID>", "columnIndex": 4 }
|
|
253
256
|
```
|
|
254
257
|
|
|
258
|
+
(If the copy already exists — e.g. it was placed by an earlier step — pass `targetNodeId: "<existing-node-id>"` instead.)
|
|
259
|
+
|
|
255
260
|
(REST fallback: see `references/api-fallback.md` — "Step 6a — Link a node to an origin on a different timeline".) This replaces the new node's meta with a full copy of the origin's, sets `meta.linkedTo`, and only works for COMMAND/EVENT/READMODEL. Once linked, wire the local copy to its neighbors with normal same-timeline `set_connection`/auto-connect calls. `eventmodeling-checking-completeness` treats any `linkedTo`-marked node it finds as this intentional pattern, never a duplicate to flag.
|
|
256
261
|
|
|
257
262
|
---
|
|
@@ -138,7 +138,7 @@ Use the `handle-comment` skill with `action=place` to post each comment. Pass:
|
|
|
138
138
|
- `type` — `COMMENT` (there is no separate question type — the text itself carries the question)
|
|
139
139
|
- `author` — `wdyt`
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Post them together, not one at a time: `handle-comment` sends every comment of a run in a single batch request (`add_comments` over MCP, `POST .../boards/:boardId/comments` over REST).
|
|
142
142
|
|
|
143
143
|
Only post questions that are **genuinely unclear or missing** — don't post observations that are clearly intentional design decisions.
|
|
144
144
|
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|
|
@@ -14,8 +14,11 @@ Ralph's runtime directory. Contains the agent loop, board poller, prompts, and C
|
|
|
14
14
|
# Claude (default)
|
|
15
15
|
node .build-kit/ralph-claude.js
|
|
16
16
|
|
|
17
|
-
# Local
|
|
18
|
-
|
|
17
|
+
# Local or self-hosted model — Ollama (run `ollama serve` first)
|
|
18
|
+
LOCAL_AI_TARGET=ollama node .build-kit/ralph-local-ai.js
|
|
19
|
+
|
|
20
|
+
# …or any OpenAI-compatible server (vLLM, LM Studio, llama.cpp, TGI)
|
|
21
|
+
LOCAL_AI_TARGET=vllm LOCAL_AI_MODEL=Qwen/Qwen3-8B node .build-kit/ralph-local-ai.js
|
|
19
22
|
|
|
20
23
|
# Custom project directory (defaults to the parent of .build-kit)
|
|
21
24
|
node .build-kit/ralph-claude.js /path/to/project
|
|
@@ -28,7 +31,8 @@ node .build-kit/ralph-claude.js /path/to/project
|
|
|
28
31
|
| File | Purpose |
|
|
29
32
|
|------|---------|
|
|
30
33
|
| `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
|
|
31
|
-
| `ralph-
|
|
34
|
+
| `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
|
|
35
|
+
| `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
|
|
32
36
|
| `ralph.sh` | Shell-based loop — alternative to the JS entry points |
|
|
33
37
|
|
|
34
38
|
**Internals** (`lib/`):
|
|
@@ -36,7 +40,7 @@ node .build-kit/ralph-claude.js /path/to/project
|
|
|
36
40
|
| File | Purpose |
|
|
37
41
|
|------|---------|
|
|
38
42
|
| `lib/ralph.js` | Shared library — board poller + loop logic; imported by the entry points |
|
|
39
|
-
| `lib/
|
|
43
|
+
| `lib/local-ai-agent.js` | Local-AI executor — called by `ralph-local-ai.js`, can also run manually |
|
|
40
44
|
| `lib/agent.sh` | Thin shell wrapper around `claude` — called by `ralph.sh` |
|
|
41
45
|
| `lib/prompt.md` | Phase 1 prompt: tells Claude how to load a slice from the board |
|
|
42
46
|
| `lib/backend-prompt.md` | Phase 2 prompt: tells Claude how to build a planned slice |
|
|
@@ -46,23 +50,74 @@ node .build-kit/ralph-claude.js /path/to/project
|
|
|
46
50
|
|
|
47
51
|
**Phase 1** — triggered when `tasks.json` has entries:
|
|
48
52
|
- The poller writes a task to `tasks.json` each time it notices a slice's status changed since the last poll
|
|
49
|
-
- The loop picks it up and runs Claude (or
|
|
53
|
+
- The loop picks it up and runs Claude (or a local model) with `prompt.md`
|
|
50
54
|
- Claude loads the slice data and updates `.slices/`
|
|
51
55
|
|
|
52
56
|
**Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
|
|
53
57
|
- The loop runs Claude with `backend-prompt.md`
|
|
54
58
|
- Claude implements the slice in the project
|
|
55
|
-
- Phase 2 is Claude-only;
|
|
59
|
+
- Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
|
|
56
60
|
|
|
57
61
|
Both phases run in a continuous loop with a 3-second idle sleep. The board poller runs concurrently in the same process, re-fetching `slicedata/slices` every `RALPH_POLL_INTERVAL_MS` (default 10s — see `lib/ralph.js`).
|
|
58
62
|
|
|
59
|
-
##
|
|
63
|
+
## Local-AI configuration
|
|
64
|
+
|
|
65
|
+
`ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
|
|
66
|
+
Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
|
|
70
|
+
LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
|
|
71
|
+
LOCAL_AI_MODEL=qwen3.5:9b # model name as the server knows it
|
|
72
|
+
LOCAL_AI_API=openai # force the wire dialect: ollama | openai (normally inferred)
|
|
73
|
+
LOCAL_AI_API_KEY=local # sent as `Authorization: Bearer` on the openai dialect
|
|
74
|
+
LOCAL_AI_NUM_CTX=32768 # ollama only — context window (default 32768)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Do not lower `LOCAL_AI_NUM_CTX`.** The MCP tool schemas are ~16k tokens on their own.
|
|
78
|
+
Ollama's own default is 4096, which silently truncates them — the model then sees a
|
|
79
|
+
fragment of the tool list and invents tool names instead of failing, which is why the
|
|
80
|
+
default here is raised rather than left to the server. On the `openai` dialect the
|
|
81
|
+
equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
|
|
82
|
+
llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## External agent commands (`--exec`)
|
|
86
|
+
|
|
87
|
+
Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
|
|
88
|
+
are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
|
|
89
|
+
already is one and only wants a prompt. They go through `ralph-exec.js` instead:
|
|
60
90
|
|
|
61
91
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
npx @eventmodelers/cli run --exec "codex exec --full-auto"
|
|
93
|
+
npx @eventmodelers/cli run --exec "opencode run"
|
|
94
|
+
|
|
95
|
+
# …or persist it and use the bare flag
|
|
96
|
+
RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
|
|
64
97
|
```
|
|
65
98
|
|
|
99
|
+
The prompt is appended to the command as one shell-quoted argument, and is also written
|
|
100
|
+
to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
|
|
101
|
+
child runs with the project dir as its cwd and inherits stdio — a harness owns its own
|
|
102
|
+
output format, so there is no condensed per-step logging here the way `ralph-claude.js`
|
|
103
|
+
has it.
|
|
104
|
+
|
|
105
|
+
Persist a default alongside the local-AI settings:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"localAi": {
|
|
110
|
+
"exec": "codex exec --full-auto"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
One caveat worth knowing before reaching for this: the kits' prompts assume Claude
|
|
116
|
+
Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
|
|
117
|
+
primitive, so `init-agents` puts the skill files where they can find them, but
|
|
118
|
+
`lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
|
|
119
|
+
a skill file rather than *invoke* it.
|
|
120
|
+
|
|
66
121
|
## Config
|
|
67
122
|
|
|
68
123
|
Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):
|
|
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
17
17
|
|
|
18
18
|
0. Do not read the entire code base. Focus on the tasks in this description.
|
|
19
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)
|
|
20
|
+
2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
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
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:**
|