@eventmodelers/cli 1.0.106 → 1.0.108

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.106",
3
+ "version": "1.0.108",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,8 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
29
29
  | `get_nodes` | `boardId`, `type?`, `name?`, `chapterId?`, `nodeIds?`, `projection?` (`"line"`) | List nodes, optionally by type and/or a partial case-insensitive title match. **Unscoped with full `meta` it is expensive** — about 2 KB per node, ~1 MB on a 500-node board — so always pass a filter or `projection: "line"`. `chapterId` scopes to one timeline — prefer this over an unscoped board-wide call whenever the step is working within one chapter (the common case); `nodeIds` fetches a known, scattered subset in one call (e.g. re-verifying exactly the nodes just touched by a batch write) instead of a full `type` refetch; `projection: "line"` maps each match to `{id, type, title, fields}` — `fields` being just the attribute names. No `cellName` — an existing node is addressed by its `id`; when a cell address is needed (placing into a cell), read it from `get_board_outline`. No `sliceStatus` — take it per column from `get_board_outline` | §3 `GET .../nodes` (REST records carry no `cellName`, `projection=line` included) |
30
30
  | `get_node` | `boardId`, `nodeId`, `projection?` (`"cells"` \| `"edges"`) | Get one node (no `cellName` — address it by `nodeId`; cell addresses come only from `get_board_outline`). `projection: "cells"` (CHAPTER nodes only) returns just `{rows, columns, cells}` instead of the full `timelineData` — use whenever only the grid/occupancy is needed, not the whole chapter; `projection: "edges"` returns just that node's inbound/outbound connections instead of `findNodeById`'s full record. Both are opt-in — omitting `projection` is the unchanged full response. An unknown id is a `NODE_NOT_FOUND` error | §3 `GET .../nodes/:nodeId` (REST returns the node as stored, without `cellName`; `404` carries `code: 'NODE_NOT_FOUND'`) |
31
31
  | `get_node_comments` | `boardId`, `nodeId` | List comments on a node | §1 `GET .../nodes/:nodeId/comments` |
32
- | `get_chat_attachment` | `boardId`, `sessionId`, `attachmentId` | The file a chat message came with — the message's `attachments` list `{id, type, ending}`. Images come back as images, a PDF as an embedded resource, csv / xml / txt / json as text. Only the agent the message was addressed to can read it (`CHAT_ATTACHMENT_NOT_FOUND`); needs the connection's `x-agent-id` (`CHAT_AGENT_REQUIRED`). Max 2 MB per file. | §15 `GET /api/org/:orgId/chat/sessions/:sessionId/attachments/:attachmentId` |
32
+ | `get_chat_attachment` | `boardId`, `sessionId`, `attachmentId` | The file a chat message came with — the message's `attachments` list `{id, type, ending}`. Images come back as images, a PDF as an embedded resource, csv / xml / txt / json as text. Only the agent the message was addressed to can read it (`CHAT_ATTACHMENT_NOT_FOUND`); needs the connection's `x-agent-id` (`CHAT_AGENT_REQUIRED`). Max 5 MB per file. | §15 `GET /api/org/:orgId/chat/sessions/:sessionId/attachments/:attachmentId` |
33
+ | `post_chat_message` | `boardId`, `text`, `replyTo?`, `sessionId?`, `preferUserId?`, `snippet?` | Write into a person's chat — a reply (`replyTo` marks their message answered) or a question you start (`preferUserId`). `text` ≤ 1000 characters. `snippet` attaches one interactive element under the text (confirm, poll, tasks, changes, link, image, code); the person's choice comes back as their next chat message. See §16 | §16 `POST /api/org/:orgId/boards/:boardId/chat/agent-messages` |
33
34
  | `get_board_events` | `boardId`, `since?`, `seq?` | A page of at most 300 board events, oldest first, as `{events, pageSize, hasMore}`. Not the whole log: while `hasMore` is true, page on by passing the last event's `seq` as `since`. `seq` returns only the event of exactly that seq | §1 `GET .../events` |
34
35
  | `submit_node_events` | `boardId`, `events[]`, `autoConnect?`, `compact?` | Create/update nodes (raw `NodeChangeEvent`/edge events). Every event property is described on the tool's own `events[]` schema — read that rather than this skill when all you need is the event shape. An event needs its own `id` (required — a fresh uuid per event, never the node id) and its `eventType`, plus the keys of its kind (`nodeId`/`meta`/`node`, or `edgeId`/`source`/`target`). The `id` keys the returned `hashes` map and makes a resubmit idempotent — the same event sent twice under one id writes a single board event. Don't send `boardId` or `timestamp` per event — the server takes the board from the call and stamps the time itself, and ignores both if sent. `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` |
35
36
  | `delete_node` | `boardId`, `nodeIds[]` | Delete nodes, applied in order — one failure doesn't stop the rest; answers `{results}`. 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) |
@@ -54,7 +55,7 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
54
55
  | `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 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) |
55
56
  | `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) |
56
57
  | `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` |
57
- | `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` |
58
+ | `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node. Storyline and beat ids are generated when omitted, and a beat may name its element by `{title, type}` instead of `refId` (same addressing as `add_scenario`; an ambiguous title is reported with its candidates). 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` |
58
59
  | `set_connection` | `boardId`, `connections[]` (each `source`, `target`, `action` `'connect'\|'remove'`), `compact?` | Add or remove type-checked directed edges, applied in order (a later entry can rely on an earlier edge); answers `{results}`, or with `compact: true` a `{connected, existed, removed, notFound, failed, errors}` tally | — (via `edges` on §3 events) |
59
60
  | `auto_connect_node` | `boardId`, `nodeIds[]` | Re-run auto-connect for nodes — incompatible neighbours come back in `skipped`, not as an error; answers `{results}` | §3 `POST .../nodes/:nodeId/auto-connect` |
60
61
  | `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` |
@@ -714,15 +715,15 @@ Append one or more storylines to a column's spec node. The spec node is auto-cre
714
715
  **Request body**: a single storyline object or an array:
715
716
  ```typescript
716
717
  {
717
- id: string
718
+ id?: string // generated when omitted
718
719
  title: string // must be unique within the spec node
719
720
  description?: string
720
721
  layout?: 'horizontal' | 'vertical'
721
722
  beats: Array<{
722
- instanceId: string // unique per beat, even when refId repeats
723
- refId: string // board node id — must belong to the same timeline
724
- type?: string
725
- title?: string
723
+ instanceId?: string // unique per beat, even when the same element repeats — generated when omitted
724
+ refId?: string // board node id — must belong to the same timeline. Omit it to address by title
725
+ type?: string // with a title lookup: narrows it to EVENT / COMMAND / READMODEL
726
+ title?: string // required when refId is omitted
726
727
  isError?: boolean // marks an alternate/error branch off the previous beat
727
728
  fields?: unknown[]
728
729
  expectEmptyList?: boolean
@@ -732,10 +733,18 @@ Append one or more storylines to a column's spec node. The spec node is auto-cre
732
733
  }
733
734
  ```
734
735
 
736
+ A beat is addressed either way, like a scenario step:
737
+ ```typescript
738
+ { refId: 'node-uuid' } // a board node — always wins, never re-resolved
739
+ { title: 'OrderPlaced', type: 'EVENT' } // resolved against this timeline's own elements
740
+ ```
741
+
742
+ A beat given as `{title, type}` is resolved server-side, so no `get_spec_info`/`get_nodes` call is needed first — and a storyline needs no ids minted by the caller. Only `EVENT`, `COMMAND` and `READMODEL` elements can be named (what `GET .../spec-info` serves); any other element type still needs its `refId`. A title matching more than one element is rejected (`STORYLINE_BEAT_AMBIGUOUS`) with the candidates rather than guessed at, and resolution runs before anything is written — a bad name leaves no empty spec node behind. Payloads that carry `id`, `instanceId` and `refId` behave exactly as before.
743
+
735
744
  **Response**:
736
745
  - `201` — `{ specNodeId, storylines, added, isNewNode }`
737
- - `400` — validation error
738
- - `404` — timeline, column, or referenced node not found
746
+ - `400` — validation error (`STORYLINE_BEAT_AMBIGUOUS`: a title matches several elements; `STORYLINE_BEAT_REF_ID_MISSING`: a beat has neither `refId` nor `title`)
747
+ - `404` — timeline, column, or referenced node / title not found
739
748
  - `409` — duplicate storyline title
740
749
 
741
750
  ---
@@ -1052,7 +1061,7 @@ Check whether an agent has pinged for a board within the last 45s. Auth: Supabas
1052
1061
 
1053
1062
  ## 15. Chat attachments
1054
1063
 
1055
- A chat message can carry one file (image: png/jpg/gif/webp, pdf, csv, xml, txt, json — max 2 MB). A `CHAT` turn's header lists it as `attachments=[{"id","type":"blob","ending"}]`; the same list is on every message of `get_chat_session` and `chat/next`. Prefer the MCP tool `get_chat_attachment`.
1064
+ A chat message can carry up to 3 files (image: png/jpg/gif/webp, pdf, csv, xml, txt, json — max 5 MB each). A `CHAT` turn's header lists them as `attachments=[{"id","type":"blob","ending"}]`; the same list is on every message of `get_chat_session` and `chat/next`. Prefer the MCP tool `get_chat_attachment`.
1056
1065
 
1057
1066
  ### GET `/api/org/:orgId/chat/sessions/:sessionId/attachments/:attachmentId?board_id=:boardId`
1058
1067
  The file itself (raw bytes, `Content-Type` per the ending). Auth: `x-token` + `x-agent-id` — only the agent the message was addressed to can read it.
@@ -1062,7 +1071,7 @@ The file itself (raw bytes, `Content-Type` per the ending). Auth: `x-token` + `x
1062
1071
  The person's side (upload, send with `attachments: [{id}]`, read) is the user-authenticated UI; an agent does not upload.
1063
1072
 
1064
1073
  ### How to handle an attachment in a chat turn
1065
- - **Read a file once, when its message arrives** — the turn whose header carries it. The CLI has already downloaded it: `attachment_files=[{"id":…,"path":…}]` gives the local path — open that with your file reader (images and PDFs included). Without an `attachment_files` entry (the download failed) use `get_chat_attachment` with `boardId`, `sessionId` (this turn's `session_id`) and `attachmentId`.
1074
+ - **Read each file once, when its message arrives** — the turn whose header carries it. A message may carry several: go through every entry. The CLI has already downloaded it: `attachment_files=[{"id":…,"path":…}]` gives the local path — open that with your file reader (images and PDFs included). Without an `attachment_files` entry (the download failed) use `get_chat_attachment` with `boardId`, `sessionId` (this turn's `session_id`) and `attachmentId`.
1066
1075
  - **Never on later turns.** `get_chat_session` lists the attachments of every message in the conversation; that is not an invitation to read them. Assume you read each one when it arrived and what you took from it is in the conversation. Fetch one again (`get_chat_attachment`) only when the person explicitly asks you to look at that file again.
1067
1076
  - The text may be empty (the file *is* the message) or point at it ("like this", "build this screen"). A screenshot or mockup is a source for the model, not something to copy: model what it shows (screens → `/storyboard-screen`, a csv or json → example data, a document → events and slices).
1068
1077
  - When you create work from it, **put what you read into the prompt's text** — element names, fields, what the screenshot shows — because a prompt turn has no attachment of its own; never refer to it as "the attached file".
@@ -1071,6 +1080,57 @@ The person's side (upload, send with `attachments: [{id}]`, read) is the user-au
1071
1080
 
1072
1081
  ---
1073
1082
 
1083
+ ## 16. Chat snippets
1084
+
1085
+ A chat message you post can carry **one snippet**: a small interactive element shown under its text. **Prefer a snippet over prose** whenever the person's answer is a yes/no, a choice between alternatives or a pick from a list — they click instead of typing, and you get an unambiguous answer. Likewise show an element, picture or command as a `link` / `image` / `code` instead of describing it. Write plain text only for answers, explanations and open questions that need words. The person clicks; their choice comes back as an **ordinary chat message** (the next `CHAT` turn), so you read it like anything they type — no special handling.
1086
+
1087
+ ### POST `/api/org/:orgId/boards/:boardId/chat/agent-messages`
1088
+ Same as the MCP tool `post_chat_message` (auth `x-token` + `x-agent-id`; the rules for who may write where are unchanged). Body: `{ text, reply_to?, session_id?, prefer_user_id?, only_if_unanswered?, snippet? }`. `text` stays **required** — it is the fallback copy and what the person reads first; keep it to a sentence and let the snippet carry the detail. `snippet` is a JSON object with a string `kind`, at most 64 KB. **Errors**: `400 CHAT_SNIPPET_INVALID` (no object / no `kind`), `400 CHAT_SNIPPET_TOO_LARGE`. The kinds below are validated by the chat, not the server: a snippet it cannot read is simply not shown, so the text must stand on its own.
1089
+
1090
+ ### Which kind when
1091
+ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or B?" or "which of these?", send the matching snippet instead.
1092
+
1093
+ | You want… | Kind | Their answer arrives as |
1094
+ |---|---|---|
1095
+ | a yes/no on one thing | `confirm` | `Yes – <headline>` / `No – <headline>` |
1096
+ | one pick out of 2+ alternatives | `poll` | the option's `message` if you gave one, else `<label> – <question>` |
1097
+ | the person to tick which of many items you should do | `tasks` | `Please do these:` + the ticked titles (or `(by id)` + ids when long) |
1098
+ | to propose edits to existing elements, for one go-ahead | `changes` | `<confirmLabel> – <headline>: <element titles>` |
1099
+ | to point at an element | `link` | nothing — it pans the canvas |
1100
+ | to show a picture | `image` | nothing |
1101
+ | to show a command or payload | `code` | nothing |
1102
+
1103
+ `confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image` and `code` are display only and never wait for anything.
1104
+
1105
+ ### Shapes
1106
+ ```typescript
1107
+ { kind: 'confirm', headline: string, yesLabel?: string, noLabel?: string } // headline is the question
1108
+ { kind: 'poll', question: string,
1109
+ options: Array<{ label: string, description?: string, message?: string }> } // 2+; message = what is sent when picked
1110
+ { kind: 'tasks', headline?: string, submitLabel?: string,
1111
+ tasks: Array<{ id: string, title: string, description?: string,
1112
+ nodeId?: string }> } // ids unique; nodeId makes the description a link that jumps to the element; 5 per page
1113
+ { kind: 'changes', headline?: string, confirmLabel?: string, // one confirm for the whole list; shown 5 per page
1114
+ items: Array<{ id: string, title: string, // title = the element's name
1115
+ nodeId?: string, // makes the title a link to the element
1116
+ slice?: string,
1117
+ changes: Array<{ field: string, from?: string, to?: string }> }> } // no from = added, no to = removed
1118
+ { kind: 'link', nodeId: string, label?: string }
1119
+ { kind: 'image', url: string, alt?: string, caption?: string } // https or png/jpg/gif/webp data URL; no svg
1120
+ { kind: 'code', code: string, language?: string, title?: string } // monospaced, with a copy button
1121
+ ```
1122
+
1123
+ ### How to use them
1124
+ - **A snippet is a question or a view, not a way to act.** Nothing a click does changes the board: a confirmed `changes` list is a go-ahead, and you then create the work (`create_prompt`) for exactly what was shown — a chat turn never edits the board itself.
1125
+ - **One snippet per message.** Need two things? Post the question as a snippet, the rest as text, or wait for the answer.
1126
+ - **Lists:** put every item in one `tasks` / `changes` snippet — it pages itself at 5 per page; do not split a long list across messages. Give each item a stable `id` you can map the answer back to.
1127
+ - **Name elements** with `nodeId` wherever you can (`tasks`, `changes`, `link`), so the person can click through to them — in a `tasks` list, put the element's name in `description` and its id in `nodeId`.
1128
+ - **Answers are text.** For a `tasks` answer, match the titles (or ids) back to your list; an element the person left unticked was declined, not forgotten. A `poll` option with a `message` of your own makes the answer unambiguous.
1129
+ - `get_chat_session` shows your snippet on the message as `snippet`, and the person's answer is the user message after it, carrying `context.snippetReply.messageId` (your message's id) — useful to tell an answer to a snippet from a new message.
1130
+ - A snippet is shown exactly as you send it — keep secrets and tokens out of `code` and `image`.
1131
+
1132
+ ---
1133
+
1074
1134
  ## Domain Events
1075
1135
 
1076
1136
  ### Snapshot Events (`src/events/SnapshotsEvents.ts`)
@@ -114,6 +114,22 @@ Stop traversal when:
114
114
 
115
115
  ---
116
116
 
117
+ ## Step 3e — Check the slices first (before any write)
118
+
119
+ Only elements in a slice with status `Created` may be changed — and an element in no slice at all is
120
+ not locked. The chain you resolved does not carry that: `get_attribute_chain` and `get_node` do not
121
+ report `sliceStatus`. Read it now, once: `mcp__eventmodelers__get_board_outline { "boardId": "$BOARD_ID", "chapterId": "$CHAPTER_ID" }`
122
+ gives every column a `sliceStatus` when a slice spans it. Look up each chain node's column.
123
+
124
+ - **Every node writable** (`Created`, or no `sliceStatus`) → continue with Step 4.
125
+ - **Any node in a column with another status** (`Done`, `Review`, `InProgress`, `Blocked`, `Planned`, `Assigned`,
126
+ `Informational`) → **do not write that node.** If the task text says the person already confirmed changing
127
+ that locked slice (*"The person confirmed changing the locked slice '…'"*, with an `origin_message_id`), you may
128
+ change those elements and only those. Otherwise write **nothing** — not even the nodes that are writable, the
129
+ chain is one logical edit — and follow `CLAUDE.md` § *Locked slices in a chat*: ask in the chat with a
130
+ `confirm` snippet (or a `changes` snippet listing the elements) naming the slice and its status, post no
131
+ `COMMENT` on the slice, and end the turn. (No chat to ask in? Change nothing and post a `COMMENT` on the slice.)
132
+
117
133
  ## Step 4 — Apply the change to the whole chain in one write
118
134
 
119
135
  Compute the updated `fields` array for **every** node in the chain first, in order (TARGET_NODE first, then backwards to SOURCE_NODE), then submit them all in a **single** `submit_node_events` call. Do not write one node, check it, and move to the next — the chain is one logical edit and `events[]` takes the whole batch.
@@ -179,7 +195,7 @@ Tell the user:
179
195
  - **Operation**: add `"<name>"` / rename `"<old>"` → `"<new>"`
180
196
  - **Chain**: list each element in order (type + title + cell)
181
197
  - **Updated**: which nodes were changed
182
- - **Skipped**: which nodes were skipped and why (field already exists / field not found)
198
+ - **Skipped**: which nodes were skipped and why (field already exists / field not found / slice not `Created` — asked to confirm)
183
199
 
184
200
  Example output:
185
201
  ```
@@ -25,6 +25,14 @@ From `$ARGUMENTS`, extract:
25
25
 
26
26
  ---
27
27
 
28
+ ## Step 1a — Locked slices
29
+
30
+ Only elements in a slice with status `Created` may be changed (no slice at all = not locked).
31
+ `get_board_outline` reports `sliceStatus` per column; read it for the chapter before writing, and for
32
+ any target in a column with another status (`Done`, `Review`, `InProgress`, …) **write nothing to it**
33
+ unless the task says the person already confirmed changing that locked slice. Otherwise ask in the chat
34
+ with a `confirm` snippet — `CLAUDE.md` § *Locked slices in a chat* — and post no `COMMENT` on the slice.
35
+
28
36
  ## Step 1b — Many targets at once
29
37
 
30
38
  `target` is singular, but the common real request is "fill in the examples across this chapter". When you have more than one target, **do not run this skill once per element and do not fetch context a node at a time.**
@@ -15,6 +15,44 @@ A **domain event** is something that happened in the business domain. Past tense
15
15
 
16
16
  ---
17
17
 
18
+ ## Chat mode — the person selects what goes on the board
19
+
20
+ **When this run has a chat** (`CHAT_SESSION_ID` is set — the request came from the board's chat), you do not place
21
+ events as soon as you find them: you **offer** them as a snippet and place only what the person ticks. Without a chat
22
+ (a terminal session) everything below Step 1 works as written — place immediately. Chat mode changes four things:
23
+
24
+ 1. **Which timeline** (Step 1a, several chapters exist): post a **`poll` snippet** — one option per chapter (its name)
25
+ plus *"A new timeline"* — instead of asking in prose, and end the turn. The pick arrives as the next chat message.
26
+ 2. **Candidate events** (Step 3a–3b): don't call Step 4 yet. Post **one** `post_chat_message` (`sessionId` =
27
+ `CHAT_SESSION_ID`) with a **`tasks` snippet** — every change you found this round, each a tickable line:
28
+ ```
29
+ mcp__eventmodelers__post_chat_message {
30
+ "boardId": "<BOARD_ID>", "sessionId": "<CHAT_SESSION_ID>",
31
+ "text": "From what you told me I found 4 events. Tick the ones that belong on the timeline.",
32
+ "snippet": { "kind": "tasks", "headline": "Add to the timeline?", "submitLabel": "Add to timeline", "tasks": [
33
+ { "id": "add:1:after:<eventNodeId>", "title": "Order Placed", "description": "After Cart Checked Out" },
34
+ { "id": "rename:<eventNodeId>", "title": "Payment Received", "description": "Renamed from Payment Done", "nodeId": "<eventNodeId>" },
35
+ { "id": "remove:<eventNodeId>", "title": "Remove Page Viewed", "description": "Not a business event", "nodeId": "<eventNodeId>" } ] } }
36
+ ```
37
+ - `title` is the event name (naming rules below), or for a removal `Remove <name>`. `description` says where it goes
38
+ or what changes; `nodeId` (existing events only) makes the description a link to it on the board.
39
+ - `id` carries the operation, because the turn that applies it remembers nothing: `add:<n>:after:<eventNodeId|start>`
40
+ (a new event right after that one, or first), `rename:<eventNodeId>`, `remove:<eventNodeId>`. Unique per task.
41
+ - Put **every** change of the round in the one snippet (it pages itself at 5). Then **end the turn** — nothing is
42
+ placed, renamed or removed while you wait. Nothing found: one line of text, no snippet.
43
+ 3. **Your one follow-up question** (Step 3d): if the answer is a pick — *"Can Payment fail?"* → a `confirm`, *"Which
44
+ comes first?"* → a `poll` — it is a snippet too; an open question stays text. The question goes in the same message
45
+ as the candidates, as its text, never as a second message.
46
+ 4. **Applying the answer.** It arrives as a `CHAT` turn (`Please do these:` + the ticked titles, or ids when the list was
47
+ long). Read your snippet back (`get_chat_session`), match the ticked tasks, and create **one prompt**: *"Apply these
48
+ /timeline changes in chapter `<CHAPTER_ID>`: `<id> | <title>` per line"*. Unticked ones were declined: drop them.
49
+ A prompt that starts with **"Apply these /timeline changes"** is the applying run: load the chapter (Step 1b) but
50
+ skip 3a–3b — for each line do Step 4 (`add` → 4a at the position after the named event, `rename` → 4b, `remove` →
51
+ 4c), then post one short chat message: what went on the board, and the next question as in item 3. This is how the
52
+ timeline keeps growing: *tell me more → a snippet to tick → applied → the next snippet.*
53
+
54
+ ---
55
+
18
56
  ## Step 1 — Gather inputs and start immediately
19
57
 
20
58
  From `$ARGUMENTS` and the conversation, extract:
@@ -44,7 +82,7 @@ mcp__eventmodelers__get_nodes { "boardId": "<BOARD_ID>", "type": "CHAPTER", "pro
44
82
 
45
83
  **If `timelineId` was not provided:**
46
84
 
47
- - If one or more chapters exist, list them by name (falling back to ID if unnamed) and ask:
85
+ - If one or more chapters exist (in a chat: a `poll` snippet, see *Chat mode*), list them by name (falling back to ID if unnamed) and ask:
48
86
  > "I found these timelines on the board: [list]. Which one do you want to continue, or should I create a new one?"
49
87
  Wait for the user's answer before proceeding.
50
88
  - If no chapters exist, proceed directly to [1c — Creating a new timeline](#1c--creating-a-new-timeline).
@@ -155,6 +193,8 @@ Ignore implementation details, system internals, and technical steps.
155
193
 
156
194
  ### 3b — Decide what to do for each candidate
157
195
 
196
+ *(In a chat, this decides what goes into the snippet — see *Chat mode* — instead of what you place now.)*
197
+
158
198
  Compare against the current `events` state:
159
199
 
160
200
  | Situation | Action |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wdyt
3
- description: Business analyst exploration of an event model board. Reads all slices, analyzes them from a business perspective, and posts questions/observations as comments on relevant nodes. Findings about a relationship between two elements are additionally drawn on the canvas as an arrow — comments carry every textual question, arrows carry the relational hints.
3
+ description: Business analyst exploration of an event model board. Reads all slices, analyzes them from a business perspective, and shows its questions/observations to the person first and, once they say so, posts them as comments on relevant nodes. Findings about a relationship between two elements are additionally drawn on the canvas as an arrow — comments carry every textual question, arrows carry the relational hints.
4
4
  ---
5
5
 
6
6
  # WDYT — What Do You Think?
@@ -123,7 +123,57 @@ Whichever of these four you raise, its comment goes on the element at the centre
123
123
 
124
124
  ---
125
125
 
126
- ## Step 4 — Comments carry every textual question; arrows carry relational hints
126
+ ## Step 4 — Show the findings first; comments only after the person says so
127
+
128
+ **When this run has a chat** (`CHAT_SESSION_ID` is set — the request came from the board's chat, as a prompt's
129
+ `origin_session_id` or as a `CHAT` turn), **nothing goes on the board yet.** Steps 4.1 and 4.2 below run only
130
+ after the person has answered. Instead:
131
+
132
+ ### 4.0 Offer the findings in the chat — a snippet, then the question
133
+
134
+ Post **one** `post_chat_message` (`sessionId` = `CHAT_SESSION_ID`, no `replyTo` in a prompt turn; `replyTo` =
135
+ this turn's `message_id` in a `CHAT` turn) with a **`tasks` snippet** — it is the list of findings *and* the
136
+ question whether they should become comments:
137
+
138
+ ```
139
+ mcp__eventmodelers__post_chat_message {
140
+ "boardId": "$BOARD_ID",
141
+ "sessionId": "<CHAT_SESSION_ID>",
142
+ "text": "I have 7 questions about Registration. Tick the ones that should go on the board as comments.",
143
+ "snippet": {
144
+ "kind": "tasks",
145
+ "headline": "Post these as comments?",
146
+ "submitLabel": "Post as comments",
147
+ "tasks": [
148
+ { "id": "c:<nodeId>:1", "title": "What happens when a customer registers without a name?", "description": "Register Customer", "nodeId": "<nodeId>" },
149
+ { "id": "c:<nodeId>:2:to:<otherNodeId>", "title": "Does this always reach the shipping step?", "description": "Order Placed → Ship Order (also drawn as an arrow)", "nodeId": "<nodeId>" }
150
+ ]
151
+ }
152
+ }
153
+ ```
154
+ - **`title`** is the finding exactly as it would be posted — one sentence, plain business language (the language rule
155
+ applies). **`description`** names the element it is about (and, for a relational finding, both), and **`nodeId`**
156
+ is that element's id (the first one, for a relational finding): the person clicks the name to jump to it on the canvas.
157
+ - **`id`** carries what you need later, because the turn that posts the comments remembers nothing:
158
+ `c:<nodeId>:<n>` for a comment on that node, and `c:<nodeId>:<n>:to:<otherNodeId>` when the finding is relational
159
+ and also gets an arrow (4.2). Unique per task.
160
+ - The snippet pages itself at 5 per page — put **every** finding in the one snippet, never split them across messages.
161
+ - Then **end the turn.** Post no comment and draw nothing while you wait; zero findings means a one-line text reply
162
+ and no snippet. The report of Step 5 is this message.
163
+
164
+ **The answer arrives as a `CHAT` turn** — `Please do these:` followed by the titles of the ticked findings (or `(by id)`
165
+ and the ids, when the list was long). Read your snippet back with `get_chat_session` (it is on your message as
166
+ `snippet`), match the ticked titles/ids to its tasks, and create **one prompt** for the ticked ones: *"Post these
167
+ /wdyt findings as comments (author wdyt) — skip Steps 1–3 and 4.0: `<id> | <title>` per line"*. Unticked findings were
168
+ declined: drop them. Nothing ticked, or a "no": acknowledge in a line.
169
+
170
+ **A prompt that starts with "Post these /wdyt findings"** is the posting run: do **not** load the model or analyse
171
+ again — for each line, the id gives the node (and the arrow target), the title is the comment text — and go straight
172
+ to 4.1 (and 4.2 for the `:to:` ones).
173
+
174
+ **No chat** (run straight from a terminal session): skip 4.0 and post right away, as 4.1 and 4.2 say.
175
+
176
+ ## Posting — comments carry every textual question; arrows carry relational hints
127
177
 
128
178
  The two channels have a strict division of labor, always applied the same way — never swap them:
129
179
 
@@ -178,7 +228,9 @@ A finding about a single element, or about a cluster, gets a comment only — do
178
228
 
179
229
  ## Step 5 — Report back to the user
180
230
 
181
- After posting all comments, give the user a concise summary:
231
+ **With a chat**, the report is the 4.0 message (a count in its text, the findings in the snippet) and the posting run
232
+ ends with one short chat message: how many comments went up, and the arrows drawn. **Without one**, after posting
233
+ all comments, give the user a concise summary:
182
234
 
183
235
  1. **Flows analysed**: count and list them by their business name
184
236
  2. **Total questions posted**: count (it is perfectly fine if this is 0 — don't force questions)
@@ -4,7 +4,7 @@ Reusable learnings accumulated while processing prompts for this board. Append n
4
4
  ones in a compressed, reusable form; only add if not already covered here.
5
5
 
6
6
  - `/place-element` requires an existing column — create one via the timeline API if missing.
7
- - `/wdyt` posts comments onto nodes — use for analysis only, not modifications.
7
+ - `/wdyt` posts comments onto nodes — use for analysis only, not modifications. In a chat it shows its findings as a snippet to tick first. A chat message that is "wdyt" always runs it; a review asked in other words may be answered in prose.
8
8
  - The `board_id`, `timeline_id`, and `organization_id` from each prompt provide full context — pass them to skills that need them.
9
9
  - If a prompt's `context.timelineId` is present and non-null, it overrules the prompt's own `timeline_id` field — it's the chapter the user was pointing at on the canvas, which can differ from whatever chapter the prompt/voice session was scoped to. Resolve `TIMELINE_ID` from `context.timelineId` first, falling back to `timeline_id` only when it's absent, before passing it to any skill.
10
10
  - Same pattern for node references: if a prompt's `context.selectedNodes` array is present and non-empty, its first entry overrules the prompt's own `node_id` field (e.g. for `/handle-comment`'s `nodeId`) — it reflects the actual canvas selection at prompt time, whereas `node_id` is only set when the prompt originated from a specific node/comment.
@@ -248,6 +248,10 @@ So on a `mode=propose` turn:
248
248
  *"You renamed Orders to take — not sure what to do with it yet. Want fields on it?"*
249
249
  If an earlier message of yours is still unanswered, fold what it proposed into this one rather
250
250
  than repeating it or staying quiet.
251
+ **Send the proposal as a snippet** so the person answers with a click, not a typed reply — a `confirm`,
252
+ or a `changes` list when it edits existing elements (`/learn-eventmodelers-api` § 16). The text above
253
+ stays as the lead-in; the click arrives as the yes in step 6. Prose alone is for when a snippet
254
+ cannot say it.
251
255
  5. **Besides that, only two things keep you silent**: `CHAT_NO_ACTIVE_SESSION` (they cleared the conversation or
252
256
  switched to another agent — never fall back to acting or to a board comment) and
253
257
  `CHAT_AWAITING_REPLY` (you wrote to them less than 3 minutes ago). Then reply `<promise>NOOP</promise>`. A turn in which you posted is not a NOOP.
@@ -84,15 +84,21 @@ header's `standalone=on|off` tells you which mode they come in.
84
84
 
85
85
  At the start of every session, read `.agent-modeling-kit/AGENTS.md` if it exists to load accumulated learnings.
86
86
 
87
- **Only touch elements in a slice whose status is `Created`.** Every other status — `Planned`,
87
+ **Only touch elements in a slice whose status is `Created` — unless the person explicitly confirmed it.** Every other status — `Planned`,
88
88
  `Assigned`, `InProgress`, `Review`, `Blocked`, `Done`, `Informational` — means someone is working
89
89
  on that slice: read it for context, but never change, move, rename or delete its elements, and
90
90
  never add scenarios, fields or examples to them. An element in no slice at all is not locked.
91
91
  `get_board_outline` returns `sliceStatus` per column (`get_nodes` does not carry it), so the
92
92
  outline read `/connect` Step 5 already makes answers this — no `list_slices`/`get_slice_data` call needed.
93
93
  If only part of what you were asked to do is locked, do the rest and name what you skipped and
94
- why; if all of it is, change nothing and post a `COMMENT` on that slice saying which status
95
- blocked it.
94
+ why. **When the work came from a chat** (the prompt carries `origin_session_id=`, or you are in a
95
+ `CHAT` turn), do not simply refuse and do not change it either: **ask the person to confirm**, in
96
+ the chat, with a `confirm` snippet (see "Locked slices in a chat" below). Their yes is what unlocks
97
+ exactly those elements — nothing else about the rule loosens. **No skill overrides this.** `/attributes`,
98
+ `/examples`, `/place-element` and the rest write through their own steps; a skill that does not mention
99
+ `sliceStatus` is not permission — check the outline before its first write, every time. A `COMMENT` on the slice is only for
100
+ when there is no chat to ask in (a prompt that did not come from one, a standalone turn): then change
101
+ nothing and comment which status blocked it. Where there is a chat, never comment — ask with the snippet.
96
102
 
97
103
  **One board read, shared by the whole turn.** Orientation first — `get_board_outline`, or `get_nodes` with
98
104
  `projection: "line"` — to establish where the work actually is, and only for the chapter this turn is about.
@@ -190,15 +196,41 @@ for it. Instead:
190
196
  `context` carries over from the message on its own):
191
197
  - **answer** — a question about the model: read what you need (cheapest read first), change
192
198
  nothing.
199
+ - **wdyt** — the message is "wdyt" (with or without a question mark, or `/wdyt`): **always** the `/wdyt`
200
+ skill, never an inline answer. Create one prompt for it (`create_prompt`, `originMessageId` = this
201
+ message) — *"Run /wdyt on `<context or chapter>`"*, the target resolved from `context=` like any other
202
+ (`timelineId` for the chapter, what they have selected). It sends its findings back as a snippet to tick
203
+ and posts nothing to the board until they answer (`/wdyt` Step 4.0). Your reply here is one line —
204
+ *"On it — looking at Registration."*
205
+ - **a skill by name** — the message starts with `/<name>` of one of your skills (the chat panel's menu
206
+ writes these: `/analyze-existing-model`, `/add-next-slice`, `/examples`, `/timeline …`,
207
+ `/html-screen …`, `/storyboard …`): the person chose that skill, so it **is** the work — create one prompt,
208
+ *"Run /<name> <the rest of the message>"*, with the target resolved from `context=` like any other
209
+ (selected elements, chapter), and say so in one line. Don't re-interpret it as a question. A skill that
210
+ needs something the message lacks (which attribute? which screen?) is a **clarify**: ask, with a
211
+ snippet if the answer is a pick. A locked slice still needs the person's confirmation
212
+ ("Locked slices in a chat" below).
213
+ - **a review in other words** ("what do you think?", "any questions on this?", "what's missing here?") —
214
+ your call: a quick take is an **answer**, in prose; a thorough review of a whole context or chapter may
215
+ be the same prompt as above. If a reply ends in a question the person answers with a click ("Want me to
216
+ turn these into comments?"), that question is a snippet, not a sentence (step 4, Snippets).
193
217
  - **clarify** — too vague to act on even with the session behind it: do no work.
194
218
  - **work** — it asks for a change to the board: create the work now, as below — a request is its
195
- own go-ahead, in every mode.
219
+ own go-ahead, in every mode — **except for a locked slice.** Before `create_prompt`, find out whether
220
+ the elements it names (and, for a change that follows connections, the ones it reaches) sit in a slice
221
+ that is not `Created`: `get_board_outline` for that chapter, `sliceStatus` per column. If so, no
222
+ prompt for those: ask first (see "Locked slices in a chat"). A request is not a go-ahead to reopen
223
+ finished work.
196
224
  - **confirm** — the message says yes to a proposal you started after a board change (`--modeling`
197
225
  proposes instead of acting on board changes; see "Propose mode" in CLAUDE-STANDALONE.md). Any
198
226
  form counts, typed or spoken ("yes", "sure", "go ahead", "do it", "ok, but only the
199
227
  scenarios"): create the work for exactly what you proposed — a partial yes, only that part; a
200
228
  no ("leave it"), nothing. A reply that changes the plan ("yes, but call it OrderSubmitted")
201
229
  confirms the changed plan. Not sure it is a yes? Treat it as a new message and ask.
230
+ The proposal may have been a **snippet** (step 4): the click arrives as an ordinary message —
231
+ `Yes – <question>` or `Apply changes – …` is a yes to exactly what that snippet showed; a `Please do
232
+ these:` list is a yes to only those items (unticked = declined); a poll pick is the chosen option.
233
+ The answer's `context.snippetReply.messageId` in `get_chat_session` names the message it answers.
202
234
  Creating work: **every board change goes through a prompt,
203
235
  however small** — never change the board in a chat turn. Create one `create_prompt` per
204
236
  independent piece of work, each with `originMessageId` = this turn's `message_id` (for a
@@ -215,16 +247,32 @@ for it. Instead:
215
247
  - no cell addresses, column letters or ids — element names are enough;
216
248
  - no hedging about what they might have meant — if you had to guess, say the guess in a few
217
249
  words ("as a dance-class booking");
218
- - at most **one** follow-up suggestion, as a short question — never a menu of options.
250
+ - at most **one** follow-up suggestion, as a short question — never a menu of options in the text.
251
+ A choice the person makes with a click is not a menu in the text: it is a **snippet** (below).
219
252
  By kind:
220
253
  - answer → the answer; a longer one only when the question really needs it, and then a short
221
254
  list, not paragraphs;
222
255
  - clarify → your question — asking back is right here: someone is waiting, so do not guess and
223
256
  do not post a board comment instead;
257
+ - wdyt → one short line that you are running `/wdyt` — its findings come as a snippet; a review asked in other words → the quick take in prose, or the same line;
224
258
  - work or confirm → one short line on what you are about to do — *"On it — adding scenarios to
225
259
  Register User."* The person sees the work cards appear under their message, so don't
226
260
  describe the work beyond that.
227
261
  - a no to a proposal → acknowledge it in a line, nothing else.
262
+ **Snippets are the default for questions the person answers with a pick.** `post_chat_message` takes
263
+ an optional `snippet` — one interactive element under your text (`/learn-eventmodelers-api` § 16
264
+ *Chat snippets* has the shapes). **Use one — do not ask in prose and wait for a typed "yes" —
265
+ whenever you would otherwise write:**
266
+ - a **yes/no question or a proposal** ("Shall I…?", "Want me to…?") → `confirm`; when the proposal is
267
+ edits to existing elements, a `changes` list (names as links, before/after per field, one confirm);
268
+ - a **choice between alternatives** ("A or B?", "which one first?") → `poll`;
269
+ - a **list of things for the person to pick from** (findings, candidates, drifts, "which of these?") → `tasks`;
270
+ - a **pointer to an element, a picture or a command** → `link` / `image` / `code`, instead of describing it.
271
+ Plain text stays for answers, explanations and open questions that need words. `text` is still
272
+ required and stays short — it is the lead-in, the snippet carries the detail. A snippet changes
273
+ nothing on the board: the click comes back as the next `CHAT` turn, and only then do you
274
+ `create_prompt`. The CLI's fallback reply (below) is text only, so a snippet is only ever sent by
275
+ your own `post_chat_message`.
228
276
  If you skip `post_chat_message`, the CLI posts your turn's final text as the reply (cut at 1000
229
277
  characters) — so if you do
230
278
  post, end the turn with nothing but `<promise>DONE</promise>`.
@@ -247,6 +295,37 @@ fields on it?"*). If the person has
247
295
  since switched to another agent (`CHAT_NOT_ADDRESSEE`), skip it; the work card already shows the
248
296
  result.
249
297
 
298
+ **Say what you did not do, every time.** The message covers everything the request asked for — not
299
+ only what got done. Anything you skipped, could not do or changed differently is named in it, with
300
+ the reason: *"Added the fields to Register User. I left Order Placed alone — see my question below."*
301
+ The work card shows "Done", which the person would otherwise read as everything went through. Never
302
+ reply `DONE` to a chat-originated prompt without having posted a message.
303
+
304
+ ### Locked slices in a chat
305
+
306
+ A request that touches an element in a slice that is not `Created` (e.g. `Done`) is not refused and
307
+ not silently turned into a comment: **you ask, explicitly, and wait for the yes.** Whichever turn
308
+ notices it — the `CHAT` turn (the outline you read already has `sliceStatus`) or the prompt turn —
309
+ does the same:
310
+
311
+ 1. **Change nothing in the locked slice.** Do everything else the request asked for.
312
+ 2. **Post one `post_chat_message`** (`replyTo` = this turn's `message_id` in a `CHAT` turn; `sessionId`
313
+ = `origin_session_id` in a prompt turn) with a **`confirm` snippet**: the text names the slice, its
314
+ status and what you would change — *"Order Placed is in the slice 'Place order', which is Done.
315
+ Changing it reopens finished work."* — and the snippet's `headline` is the question, with a
316
+ `yesLabel` that says what happens (*"Yes, change it"*, `noLabel` *"No, leave it"*). When the change is
317
+ several edits, a `changes` snippet (title = element as link, slice, before/after, one confirm) shows
318
+ exactly what will be touched instead.
319
+ 3. **Reply `DONE`** — a turn that asked is finished; the question is the result. **No `COMMENT` on the
320
+ slice, not now and not instead of asking** — the snippet in the chat is the only thing you post.
321
+ 4. **The click arrives as a `CHAT` turn** (`Yes, change it – …` / `Apply changes – …` is a yes, `No, leave it
322
+ – …` a no). On a yes, create the prompt for exactly the elements you asked about
323
+ (`originMessageId` = the yes) and **write the confirmation into the prompt's text**: *"The person
324
+ confirmed changing the locked slice 'Place order' (status Done): …"*. A prompt turn whose text carries
325
+ that confirmation, with `origin_message_id` pointing at the yes, may change those elements — and only
326
+ those; any other locked slice still needs its own yes. On a no, acknowledge in a line and drop it.
327
+ Not clearly a yes? Ask again, don't change it.
328
+
250
329
  ## Standalone board-change turns — see `CLAUDE-STANDALONE.md`
251
330
 
252
331
  These turns start with `BOARD_CHANGE` (the board changed) or `BOARD_REVIEW` (nothing has changed