@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.
Files changed (32) hide show
  1. package/README.md +7 -5
  2. package/cli.js +80 -28
  3. package/package.json +1 -1
  4. package/shared/build-kit/README.md +64 -9
  5. package/shared/build-kit/lib/local-ai-agent.js +281 -0
  6. package/shared/build-kit/lib/ralph.js +1 -1
  7. package/shared/build-kit/ralph-exec.js +92 -0
  8. package/shared/build-kit/ralph-local-ai.js +40 -0
  9. package/shared/build-kit/realtime-agent.js +1 -1
  10. package/shared/skills/learn-eventmodelers-api/SKILL.md +93 -43
  11. package/shared/skills/update-slice-status/SKILL.md +8 -0
  12. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
  13. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
  14. package/stacks/bridge/templates/bridge/lib/AGENT.md +1 -1
  15. package/stacks/bridge/templates/bridge/ralph-local-ai.js +43 -0
  16. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
  17. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +11 -1
  18. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +8 -3
  19. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
  20. package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
  21. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
  22. package/stacks/react/templates/build-kit/README.md +64 -9
  23. package/stacks/react/templates/build-kit/lib/prompt.md +1 -1
  24. package/stacks/react/templates/build-kit/lib/ralph.js +1 -1
  25. package/stacks/react/templates/build-kit/ralph-local-ai.js +40 -0
  26. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
  27. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -1
  28. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
  29. package/shared/build-kit/lib/ollama-agent.js +0 -147
  30. package/shared/build-kit/ralph-ollama.js +0 -40
  31. package/stacks/bridge/templates/bridge/ralph-ollama.js +0 -43
  32. 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 | — (no REST equivalent; MCP-only) |
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?` | 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 | §2 `POST .../chapters` |
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`, `sliceId`, `newStatus` | Change a SLICE_BORDER's `sliceStatus` | — (via `node:changed` event, §3) |
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` | Link two existing same-type nodes: `targetNodeId` is replaced with a full copy of `nodeId`'s meta plus `meta.linkedTo`. Linking means first create, then link | §3 `POST .../nodes/:nodeId/link` |
58
- | `add_comment` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'`), `author?` | Add a comment — word the `text` as a question to flag gaps/edge cases during review; there is no separate `QUESTION` type | (via comment events) |
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**: `{ position?: { x: number, y: number } }`
229
- **Response**: `200` — chapter data
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 a lane (row) to a timeline.
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
- Link two existing same-type nodes — the REST fallback for `link_element`. Linking means first create, then link: `targetNodeId` must already exist. It's replaced with a full copy of `:nodeId`'s meta (not a merge) plus `meta.linkedTo`. COMMAND/EVENT/READMODEL only; `:nodeId` must not itself already be a linked copy.
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
- **Response**: `200` `{ nodeId, linkedTo, type }` · `400` — missing `targetNodeId`, type mismatch, self-link, unsupported element type, or the original is itself a linked copy · `404` the original or `targetNodeId` doesn't exist
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 + 3 nodes automatically placed).
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/contexts/:contextName/slices/:sliceName/scenarios`
562
- Append a Given-When-Then scenario to a spec node.
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: string
617
+ id?: string // generated when omitted
568
618
  title: string
569
619
  vertical?: boolean
570
620
  examples?: unknown[]
571
- given: string[] // nodeIds — must be EVENTs from same timeline
572
- when: string[] // nodeIds — at most one COMMAND; empty if then has READMODEL
573
- then: string[] // nodeIds — EVENTs only OR exactly one READMODEL (not mixed)
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` — `{ scenario, scenarios, specNodeId, isNewNode: boolean }`
641
+ - `201` — `{ specNodeId, scenarios, added, isNewNode: boolean }`
585
642
  - `400` — validation error
586
- - `404` — context or slice not found
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/contexts/:contextName/spec-info`
592
- Get valid elements for a context (by name lookup).
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` — `{ chapterId: string, elements: ElementRecord[] }`
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 --ollama`
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 one request per comment there is no batch endpoint for comments. Fire them sequentially, not in a single payload.
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**: place the new node normally (Step 7, same title/type as the origin), then call `link_element` to mark it as a copy of the origin node:
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>", "targetNodeId": "<newly-placed-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
- The comment API has no batch endpoint `handle-comment` sends one request per comment. Fire them sequentially.
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 Ollama model — run `ollama serve` first
18
- OLLAMA_MODEL=qwen3.5:9b node .build-kit/ralph-ollama.js
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-ollama.js` | Runs the full loop using a local Ollama model |
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/ollama-agent.js` | Ollama executor — called by `ralph-ollama.js`, can also run manually |
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 Ollama) with `prompt.md`
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; Ollama mode skips it (ollama-agent handles its own queue)
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
- ## Ollama configuration
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
- OLLAMA_MODEL=qwen3.5:9b # model to use (default: qwen3.5:9b)
63
- OLLAMA_URL=http://host:11434 # Ollama server URL (default: http://localhost:11434)
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:**