@eventmodelers/cli 1.0.107 → 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 +1 -1
- package/shared/skills/learn-eventmodelers-api/SKILL.md +68 -8
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +17 -1
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +8 -0
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +41 -1
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +55 -3
- package/stacks/modeling-kit/templates/kit/AGENTS.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +4 -0
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +84 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
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": {
|
|
@@ -30,6 +30,7 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
|
|
|
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
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
|
|
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
|
|
723
|
-
refId
|
|
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
|
---
|
|
@@ -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
|
|
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 —
|
|
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
|
-
|
|
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
|
|
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
|
|
95
|
-
|
|
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
|