@eventmodelers/cli 1.0.86 → 1.0.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,8 +125,8 @@ npx @eventmodelers/cli init --stack <name> # scaffold a stack + install
125
125
  npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made demo model in the kit's .slices/ to build against
126
126
  npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
127
127
  npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
128
- npx @eventmodelers/cli run --local-ai [target] # same, via a local/self-hosted model (ralph-local-ai.js)
129
- npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js)
128
+ npx @eventmodelers/cli run --local-ai [target] # board work only via a local/self-hosted model (ralph-local-ai.js) — does NOT build slices; build locally with --exec
129
+ npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
130
130
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
131
131
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
132
132
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
package/cli.js CHANGED
@@ -3193,8 +3193,8 @@ credentialFlags(program
3193
3193
  credentialFlags(program
3194
3194
  .command('run')
3195
3195
  .description('Start the agent loop from the installed kit dir — build-kit stacks: ralph-claude.js (default); modeling-kit: --modeling, or --standalone, which needs no install at all')
3196
- .option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Claude remains the default when this flag is absent.`)
3197
- .option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent.')
3196
+ .option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Board work only: --local-ai never builds a Planned slice (no file, shell or git tools). To build code with a local model, use --exec with a coding harness pointed at it, e.g. "codex exec --oss --full-auto". Claude remains the default when this flag is absent.`)
3197
+ .option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent. With agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) — without it no slice cost is recorded.')
3198
3198
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3199
3199
  .option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
3200
3200
  .option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--global).')
@@ -3907,7 +3907,7 @@ program
3907
3907
  program
3908
3908
  .command('config')
3909
3909
  .description('Print the fully resolved config (merged across the directory hierarchy + EVENTMODELERS_* env vars), with the token masked')
3910
- .option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all.")
3910
+ .option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all. A non-Claude agent via run --exec is only traced when its command includes the harness's JSON output flag, e.g. \"codex exec --json --full-auto\".")
3911
3911
  .action((opts, command) => {
3912
3912
  const cwd = process.cwd();
3913
3913
  const kitDir = findInstalledKitDir(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.86",
3
+ "version": "1.0.88",
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": {
@@ -51,7 +51,7 @@ node .build-kit/ralph-claude.js /path/to/project
51
51
  **Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
52
52
  - The loop runs Claude with `backend-prompt.md`
53
53
  - Claude implements the slice in the project
54
- - Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
54
+ - Phase 2 (building slices) never runs with `--local-ai` — see *`--local-ai` does not build code*. Build with `--exec` instead
55
55
 
56
56
  Both phases run in a continuous loop with a 3-second idle sleep. The realtime agent runs concurrently in the same process.
57
57
 
@@ -72,6 +72,34 @@ node .build-kit/realtime-agent.js
72
72
  `ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
73
73
  Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
74
74
 
75
+ ### `--local-ai` does not build code
76
+
77
+ **`--local-ai` works on the board only. It never builds a slice.** Planned slices stay
78
+ Planned. That is by design, not a bug:
79
+
80
+ - **Planned slices are never handed to it.** `ralph-local-ai.js` gives `startRalph` no
81
+ `onPlannedSlice`, so the loop only feeds it `tasks.json` prompts.
82
+ - **It has no coding tools.** `lib/local-ai-agent.js` exposes only the board MCP tools:
83
+ no file read/write, no shell, no git, no test run.
84
+ - **Its system prompt forbids file and shell access.** Such a request is answered with
85
+ `Blocked: …`.
86
+ - **A prompt stops after 12 tool-call rounds.** That is enough for board edits, but far
87
+ short of a slice build.
88
+
89
+ Here the kit is the agent loop and the model only picks tools. Building would mean
90
+ rewriting a full coding agent inside the kit, so it is left out on purpose.
91
+
92
+ **To build with a local model, use `--exec`** with a coding harness pointed at that model
93
+ (see *External agent commands* below):
94
+
95
+ ```bash
96
+ npx @eventmodelers/cli run --exec "codex exec --oss --json --full-auto" # Codex on local Ollama
97
+ npx @eventmodelers/cli run --exec "opencode run --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
98
+ ```
99
+
100
+ Or keep Claude Code as the builder and point `anthropicBaseUrl` at a local endpoint that
101
+ speaks the Anthropic API.
102
+
75
103
  ```bash
76
104
  LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
77
105
  LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
@@ -109,6 +137,20 @@ child runs with the project dir as its cwd and inherits stdio — a harness owns
109
137
  output format, so there is no condensed per-step logging here the way `ralph-claude.js`
110
138
  has it.
111
139
 
140
+ **Agent tracing needs the harness's JSON output flag.** With agent tracing on
141
+ (`eventmodelers config --agent-tracing on`), slice builds are costed just like with Claude —
142
+ but only if the harness runs in its JSON output mode. Its normal output has no token counts,
143
+ so without the flag a turn is not traced (the runner logs a hint). Add the flag yourself:
144
+
145
+ ```bash
146
+ npx @eventmodelers/cli run --exec "codex exec --json --full-auto"
147
+ npx @eventmodelers/cli run --exec "opencode run --auto --format json"
148
+ npx @eventmodelers/cli run --exec "gemini --yolo --output-format stream-json -p"
149
+ ```
150
+
151
+ The terminal then shows the raw JSON events. Only OpenCode reports a dollar cost; Codex and
152
+ Gemini turns record tokens and duration with no cost. See `agent-tracking.md`.
153
+
112
154
  Persist a default alongside the local-AI settings:
113
155
 
114
156
  ```json
@@ -1,5 +1,5 @@
1
1
  // What a slice costs: one trace per build turn against one slice, posted to the platform's
2
- // /api/org/:orgId/boards/:boardId/agent-traces. Only the runner can see what a turn cost, so it reports it.
2
+ // /api/org/:orgId/agent-traces. Only the runner can see what a turn cost, so it reports it.
3
3
  //
4
4
  // Best effort: each trace is appended to a local JSONL file, then POSTed once. No retries, no
5
5
  // buffering. A failed upload is logged and forgotten; the file is the record.
@@ -45,16 +45,78 @@ export function usageFromLocalAi(raw, durationMs) {
45
45
  };
46
46
  }
47
47
 
48
+ /** One event from an external harness's JSON output (ralph-exec.js), or null if it carries no usage.
49
+ * Matched on shape, not on the command name, so a wrapper script around a harness still counts:
50
+ * - Codex `codex exec --json`: `turn.completed` with `usage` (input_tokens includes the cached ones)
51
+ * - OpenCode `opencode run --format json`: `step_finish` per model step, with tokens and cost
52
+ * - Gemini CLI `--output-format stream-json`: the final `result` with `stats`;
53
+ * `--output-format json`: one object whose `stats.models` holds tokens per model
54
+ * Only OpenCode reports a cost — the others leave costUsd null rather than guessing a price. */
55
+ export function usageFromHarnessEvent(msg) {
56
+ if (msg?.type === 'turn.completed' && msg.usage) {
57
+ const u = msg.usage;
58
+ const cached = n(u.cached_input_tokens);
59
+ return { inputTokens: Math.max(0, n(u.input_tokens) - cached), outputTokens: n(u.output_tokens), cacheReadTokens: cached };
60
+ }
61
+ if (msg?.type === 'step_finish' && msg.part?.tokens) {
62
+ const t = msg.part.tokens;
63
+ return {
64
+ inputTokens: n(t.input),
65
+ outputTokens: n(t.output) + n(t.reasoning),
66
+ cacheReadTokens: n(t.cache?.read),
67
+ cacheWriteTokens: n(t.cache?.write),
68
+ costUsd: Number.isFinite(msg.part.cost) ? msg.part.cost : null,
69
+ };
70
+ }
71
+ if (msg?.type === 'result' && msg.stats && !('total_cost_usd' in msg)) {
72
+ const s = msg.stats;
73
+ return {
74
+ inputTokens: n(s.input_tokens),
75
+ outputTokens: n(s.output_tokens),
76
+ durationMs: Number.isFinite(s.duration_ms) ? s.duration_ms : null,
77
+ };
78
+ }
79
+ if (msg?.stats?.models && typeof msg.stats.models === 'object') {
80
+ const models = Object.entries(msg.stats.models);
81
+ const tok = (key) => models.reduce((sum, [, m]) => sum + n(m?.tokens?.[key]), 0);
82
+ const cached = tok('cached');
83
+ const [dominant] = models.sort((a, b) => n(b[1]?.tokens?.candidates) - n(a[1]?.tokens?.candidates))[0] ?? [];
84
+ return {
85
+ inputTokens: Math.max(0, tok('prompt') - cached),
86
+ outputTokens: tok('candidates') + tok('thoughts'),
87
+ cacheReadTokens: cached,
88
+ model: dominant ?? null,
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+
94
+ /** Sums the usage of every event in one turn. A field no event reported stays null (cost, model)
95
+ * or 0 (tokens), so a harness without a cost does not show up as a free turn. */
96
+ export function sumUsage(parts) {
97
+ if (!parts.length) return null;
98
+ const add = (key) => parts.reduce((sum, p) => sum + n(p[key]), 0);
99
+ const costs = parts.map((p) => p.costUsd).filter(Number.isFinite);
100
+ return {
101
+ inputTokens: add('inputTokens'),
102
+ outputTokens: add('outputTokens'),
103
+ cacheReadTokens: add('cacheReadTokens'),
104
+ cacheWriteTokens: add('cacheWriteTokens'),
105
+ costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
106
+ durationMs: parts.findLast((p) => Number.isFinite(p.durationMs))?.durationMs ?? null,
107
+ model: parts.findLast((p) => p.model)?.model ?? null,
108
+ };
109
+ }
110
+
48
111
  /** Records slice build turns. The agent id is the session: the platform requires one, and a
49
112
  * restart of the same build agent is still the same agent building the same slices. */
50
113
  export function createSliceTracer({ baseUrl, token, organizationId, agentId, traceFile, log = () => {}, enabled = true } = {}) {
51
114
  const canUpload = !!(enabled && baseUrl && token && organizationId && agentId);
52
115
 
53
116
  async function post(trace) {
54
- // The platform files a trace under the board in its path — a turn without a board has nowhere to go.
55
- if (!canUpload || !trace.boardId) return;
117
+ if (!canUpload) return;
56
118
  try {
57
- const res = await fetch(`${baseUrl}/api/org/${organizationId}/boards/${trace.boardId}/agent-traces`, {
119
+ const res = await fetch(`${baseUrl}/api/org/${organizationId}/agent-traces`, {
58
120
  method: 'POST',
59
121
  headers: { 'Content-Type': 'application/json', 'x-token': token, 'x-agent-id': agentId },
60
122
  body: JSON.stringify({ traces: [trace] }),
@@ -18,6 +18,7 @@
18
18
  // Or persist it as localAi.exec in .eventmodelers/config.json.
19
19
 
20
20
  import { startRalph, loadLocalConfig, resolveAgentIdentity } from './lib/ralph.js';
21
+ import { createSliceTracer, usageFromHarnessEvent, sumUsage } from './lib/tracing.js';
21
22
  import { spawn } from 'child_process';
22
23
  import { writeFileSync, mkdtempSync } from 'fs';
23
24
  import { tmpdir } from 'os';
@@ -52,6 +53,21 @@ const childEnv = {
52
53
  ...(cfg.agentId && !localOnly ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
53
54
  };
54
55
 
56
+ // What each slice build turn cost — see lib/tracing.js. Same opt-in as ralph-claude.js, but a
57
+ // harness only reports usage in its JSON output mode (`codex exec --json`, `opencode run --format
58
+ // json`, `gemini --output-format stream-json`), so without that flag a turn has nothing to record.
59
+ const tracingOn = !localOnly && cfg.agentTracing === true;
60
+ if (!tracingOn && !localOnly) console.log('[ralph-exec] agent tracing off — no slice cost data is sent (turn on: eventmodelers config --agent-tracing on)');
61
+ const tracer = createSliceTracer({
62
+ baseUrl: cfg.baseUrl,
63
+ token: cfg.token,
64
+ organizationId: cfg.organizationId,
65
+ agentId: cfg.agentId,
66
+ traceFile: join(projectDir, '.eventmodelers', 'trace', 'slices.jsonl'),
67
+ log: (line) => console.log(`[ralph-exec] ${line}`),
68
+ enabled: tracingOn,
69
+ });
70
+
55
71
  const promptDir = mkdtempSync(join(tmpdir(), 'ralph-exec-'));
56
72
 
57
73
  // POSIX single-quote escaping: close, insert an escaped quote, reopen. The prompt is
@@ -62,22 +78,59 @@ function shellQuote(s) {
62
78
 
63
79
  console.log(`[ralph-exec] command: ${execCmd}`);
64
80
 
65
- function runExec(prompt) {
81
+ // Parses a harness's stdout for usage while still passing it through. Most harnesses emit JSONL;
82
+ // Gemini's `--output-format json` is one pretty-printed object, hence the whole-output fallback.
83
+ function collectUsage(stream) {
84
+ const parts = [];
85
+ let buffer = '';
86
+ let all = '';
87
+ const take = (text) => {
88
+ try { const u = usageFromHarnessEvent(JSON.parse(text)); if (u) parts.push(u); } catch { /* not JSON */ }
89
+ };
90
+ stream.on('data', (chunk) => {
91
+ process.stdout.write(chunk);
92
+ const text = chunk.toString();
93
+ all += text;
94
+ buffer += text;
95
+ const lines = buffer.split('\n');
96
+ buffer = lines.pop();
97
+ for (const line of lines) if (line.trim()) take(line);
98
+ });
99
+ return () => {
100
+ if (buffer.trim()) take(buffer);
101
+ if (!parts.length && all.trim()) take(all);
102
+ return sumUsage(parts);
103
+ };
104
+ }
105
+
106
+ // `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
107
+ function runExec(prompt, slice = null) {
66
108
  return new Promise((resolvePromise, reject) => {
67
109
  const full = inlineHeader + prompt;
68
110
  const promptFile = join(promptDir, 'prompt.md');
69
111
  writeFileSync(promptFile, full);
112
+ const traced = tracingOn && !!slice;
113
+ const startedAt = Date.now();
70
114
 
71
115
  // stdio inherit: the harness owns its own output format, and there is no
72
116
  // cross-harness stream schema to parse into the condensed per-step logging
73
- // that ralph-claude.js does — so it goes straight through.
117
+ // that ralph-claude.js does — so it goes straight through. A traced turn pipes
118
+ // stdout instead, only to read usage from it; the output is still echoed as is.
74
119
  const proc = spawn(`${execCmd} ${shellQuote(full)}`, {
75
120
  cwd: projectDir,
76
- stdio: 'inherit',
121
+ stdio: ['inherit', traced ? 'pipe' : 'inherit', 'inherit'],
77
122
  shell: true,
78
123
  env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
79
124
  });
80
- proc.on('close', (code) => (code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`))));
125
+ const usage = traced ? collectUsage(proc.stdout) : null;
126
+ proc.on('close', (code) => {
127
+ if (traced) {
128
+ const u = usage();
129
+ if (u) tracer.record({ ...slice, status: code === 0 ? 'ok' : 'error' }, { ...u, durationMs: u.durationMs ?? Date.now() - startedAt });
130
+ else console.log('[ralph-exec] trace: no usage in the command output — run the harness in its JSON output mode to record slice cost');
131
+ }
132
+ code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`));
133
+ });
81
134
  proc.on('error', reject);
82
135
  });
83
136
  }
@@ -27,8 +27,7 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
27
27
  | `get_nodes` | `boardId`, `type?`, `name?`, `chapterId?`, `nodeIds?`, `projection?` (`"line"`) | List nodes, optionally by type and/or a partial case-insensitive title match. `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}` only. Every node carries `cellName`, its cell address in the chapter it is placed in (absent when unplaced) — read the cell names the cell-addressed tools need from here, never derive or invent one | §3 `GET .../nodes` |
28
28
  | `get_node` | `boardId`, `nodeId`, `projection?` (`"cells"` \| `"edges"`) | Get one node — the full response carries `cellName`, its cell address in the chapter it is placed in. `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 | §3 `GET .../nodes/:nodeId` |
29
29
  | `get_node_comments` | `boardId`, `nodeId` | List comments on a node | §1 `GET .../nodes/:nodeId/comments` |
30
- | `get_board_events` | `boardId` | All board events, in sequence | §1 `GET .../events` |
31
- | `search_board_events` | `boardId`, `name` | Search events by node name | §1 `GET .../events/search` |
30
+ | `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` |
32
31
  | `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` |
33
32
  | `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) |
34
33
  | `create_drawing` | `boardId`, `drawings[]` (each `kind`, `x`, `y`, `width`, `height`, ...) | Freehand canvas annotation (path/rect/text/sticky) — never placed in a cell. An annotation is rarely one stroke: a loop plus its arrows and label is one call with several `drawings` entries, applied in order, not three calls | — (REST `POST .../drawing/draw` accepts a single drawing or an array) |
@@ -59,9 +58,8 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
59
58
  | `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` |
60
59
  | `add_comment` | `boardId`, `comments[]` (each `nodeId`, `text`, `type?` `'COMMENT'\|'TASK'\|'QUESTION'`, `author?`) | Add comments on any nodes of one board — `QUESTION` is the type for a gap/edge case raised during review, and a review posts all its questions in one call; answers `{results}` in request order | §1 `POST .../boards/:boardId/comments` (batch) |
61
60
  | `update_comment` | `boardId`, `nodeId`, `commentId`, `action` (`'resolve'\|'delete'`) | Resolve or delete a comment | — (via comment events) |
62
- | `create_screen` | `boardId`, `screens[]` (each `contentType` `'image'\|'sketch'\|'html'`, `chapterId`, `cellId?`/`cellName?`, `nodeId?`, content fields `imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`, `title?`, `description?`, `fields?`), `autoConnect?` | Create + place new screen nodes (SCREEN or HTML_SCREEN) with their content, in one call; entries applied in order, one failure doesn't stop the rest; answers `{results}`. `title` names the node (`meta.title`) in the same call — no follow-up `node:changed` just to label the screen. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
63
- | `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages` | §4 `POST .../images/:id/sketch` + `image-nodes` |
64
- | `add_field_examples` | `boardId`, `nodeId?`, `name?`, `cellName?`, `timelineId?` | Fill empty field examples using linked-node context | — (MCP-only convenience) |
61
+ | `create_screen` | `boardId`, `screens[]` (each `contentType` `'image'\|'sketch'\|'html'`, `chapterId`, `cellId?`/`cellName?`, `nodeId?`, content fields `imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`, `title?`, `description?`, `fields?`), `autoConnect?` | Create + place new screen nodes (SCREEN or HTML_SCREEN) with their content, in one call; each HTML page at most 6000 characters (`HTML_SCREEN_PAGE_TOO_LONG` otherwise); entries applied in order, one failure doesn't stop the rest; answers `{results}`. `title` names the node (`meta.title`) in the same call — no follow-up `node:changed` just to label the screen. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
62
+ | `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages`; each HTML page at most 6000 characters (`HTML_SCREEN_PAGE_TOO_LONG` otherwise) | §4 `POST .../images/:id/sketch` + `image-nodes` |
65
63
  | `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source. Both cell names must come from a node's reported `cellName` (`get_board_outline`, `get_nodes`, `get_node`) — a guessed address that happens to exist silently resolves the wrong chain | — (MCP-only convenience) |
66
64
  | `get_image_snapshot_description` | `boardId`, `nodeId` | Load the `{elements:[...]}` sketch description from storage | — (reads what §4 sketch endpoints write) |
67
65
  | `validate_slice_data` | `sliceData` | Offline validation of a `SliceDataOutput` payload — no board access | — (MCP-only, pure function) |
@@ -188,18 +186,27 @@ Delete a board.
188
186
 
189
187
  ---
190
188
 
191
- ### GET `/api/org/:orgId/boards/:boardId/events/search`
192
- Search events by node name.
193
-
194
- **Query params**: `name` (string)
195
- **Response**: `200` — matching event array
196
-
197
- ---
198
-
199
189
  ### GET `/api/org/:orgId/boards/:boardId/events`
200
- Get all board events in sequence.
190
+ Get a page of board events in sequence order — **at most 300 per call**, oldest first. One call is not the whole log: to read it all, pass the last event's `seq` as `since` and repeat while the `X-Has-More` response header is `true`. `X-Page-Size` carries the page size that applied.
201
191
 
202
- **Response**: `200` — event array
192
+ **Query params**:
193
+ - `since` (integer, optional) — only events with a `seq` greater than this
194
+ - `skipSilent` (boolean, default `true`) — `false` also includes events marked silent
195
+ - `seq` (integer, optional) — only the event of exactly this seq (silent or not); `since` and `skipSilent` are ignored
196
+
197
+ **Response**: `200` — event array (at most 300; at most one with `seq`, empty if there is none — `X-Has-More` is then `false`)
198
+ **Errors**: `400` `EVENT_SEQ_INVALID` — `seq` is not a non-negative integer
199
+
200
+ ```bash
201
+ since=0
202
+ while :; do
203
+ more=$(curl -s -D - -o page.json "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/events?since=$since" -H "x-token: $TOKEN" \
204
+ | tr -d '\r' | awk -F': ' 'tolower($1)=="x-has-more"{print $2}')
205
+ # ...use page.json...
206
+ [ "$more" = "true" ] || break
207
+ since=$(jq '.[-1].seq' page.json)
208
+ done
209
+ ```
203
210
 
204
211
  ---
205
212
 
@@ -317,6 +317,8 @@ mcp__eventmodelers__create_screen {
317
317
 
318
318
  **Never pass an empty `pages` array** — an empty array produces a blank placeholder, same as in `html-screen`/`eventmodeling-storyboarding-events`.
319
319
 
320
+ **Keep every reconstructed page at most 6000 characters per page (the server rejects a longer page with `400 HTML_SCREEN_PAGE_TOO_LONG` — split the screen into more pages or trim the markup instead).** A reconstructed real-world page easily runs longer — keep only the structure and content the screen is about, not every nav link, footer, or repeated list item.
321
+
320
322
  **Image path — `renderMode == "image"` only, when the user explicitly asked for screenshots:**
321
323
 
322
324
  ```
@@ -245,7 +245,7 @@ The MCP `create_screen` call above already sets the node's `title` and `meta.fie
245
245
 
246
246
  A storyboard screen is placed at a *provisional* position — Steps 4 and 5 wire it to its COMMAND / READMODEL once those exist, and may move it first. Pass `autoConnect: false` on `create_screen` here so the placement doesn't pre-wire the screen to whatever happens to sit in the adjacent column; the real `SCREEN → COMMAND` and `READMODEL → SCREEN` edges are created deliberately in Steps 4 and 5. When creating several screens whose HTML is already authored, put them all in one `create_screen` call's `screens` array with `autoConnect: false`.
247
247
 
248
- Design the page(s) as real HTML/CSS, following the `html-screen` skill's guidance: write full-size markup (16px body text, generous padding — the canvas scales it down, don't shrink it yourself), one complete self-contained fragment per page (no `<html>`/`<head>`/`<body>` wrapper — the canvas adds those), no `<script>`/inline handlers (stripped server-side), and Bulma CSS classes (`title`, `button`, `is-primary`, `field`/`control`/`input`, etc. — remember heading size modifiers like `class="title is-1"`) since Bulma 0.9.4 is loaded by default. Every page MUST include real field labels matching the actual event/command fields this screen captures or displays, and at least one primary action (submit/confirm button) for command screens.
248
+ Design the page(s) as real HTML/CSS, following the `html-screen` skill's guidance: write full-size markup (16px body text, generous padding — the canvas scales it down, don't shrink it yourself), one complete self-contained fragment per page (no `<html>`/`<head>`/`<body>` wrapper — the canvas adds those), no `<script>`/inline handlers (stripped server-side), and Bulma CSS classes (`title`, `button`, `is-primary`, `field`/`control`/`input`, etc. — remember heading size modifiers like `class="title is-1"`) since Bulma 0.9.4 is loaded by default. Keep every page at most 6000 characters per page (the server rejects a longer page with `400 HTML_SCREEN_PAGE_TOO_LONG` — split the screen into more pages or trim the markup instead). Every page MUST include real field labels matching the actual event/command fields this screen captures or displays, and at least one primary action (submit/confirm button) for command screens.
249
249
 
250
250
  > **CRITICAL: NEVER pass an empty `pages` array.** An empty pages array produces a blank placeholder and is always wrong. You MUST design and include actual page markup before calling the render API.
251
251
 
@@ -31,31 +31,21 @@ From `$ARGUMENTS`, extract:
31
31
 
32
32
  1. Read the chapter **once**: `mcp__eventmodelers__get_nodes { "boardId": "$BOARD_ID", "chapterId": "$CHAPTER_ID" }`. That single response carries every element's `meta.fields` *including the `example` values already filled in* — which is exactly the canonical-value pool Step 3c asks for, for the whole chapter, in one call.
33
33
  2. Pick the canonical value per field name from that pool (e.g. `customerId: "cust-123"`, `email: "jane@example.com"`) before writing anything, so every element ends up consistent.
34
- 3. Then fill the targets. `add_field_examples` is still the preferred writer, one call per target — but it is the *only* per-target call you should be making. If you are generating the values yourself instead, batch every element's update into a single `submit_node_events { events: [...] }` call.
34
+ 3. Then generate every target's values and write them all in a single `submit_node_events { events: [...] }` call — one `node:changed` event per element, never one call per element.
35
35
 
36
36
  A run that opens each element with its own `get_node` to "read existing examples first" is doing step 1 N times over.
37
37
 
38
38
  ---
39
39
 
40
- ## Step 2 — Resolve and generate examples (prefer MCP)
40
+ ## Step 2 — Resolve the element (prefer MCP)
41
41
 
42
- `add_field_examples` is a whole-algorithm convenience tool: it resolves the node, loads linked neighbours for cross-element consistency, fills any empty field examples, and writes the result back — collapsing the entire "find node → find linked nodes → build examples → submit_node_events" flow (Steps 2–5 below) into one call. Call it with whichever identifier matches `target`:
42
+ Resolve `target` to a node, with whichever identifier it is:
43
43
 
44
- - `target` is a UUID → pass `nodeId`
45
- - `target` is a name → pass `name`
46
- - `target` is a cell name (e.g. `B3`) → pass `cellName` + `timelineId` (the chapter id — if multiple chapters exist on the board, resolve which one first using 2c-fallback's chapter lookup, or `mcp__eventmodelers__get_nodes { "boardId": "$BOARD_ID", "type": "CHAPTER" }`, and ask the user if ambiguous). Only pass a cell name you were given or read back as a node's `cellName` (`get_board_outline`/`get_nodes`/`get_node` all report it) — if all you have is a node id or title, pass that instead rather than constructing an address.
44
+ - `target` is a UUID → `mcp__eventmodelers__get_node { "boardId": "$BOARD_ID", "nodeId": "<target>" }`
45
+ - `target` is a name → `mcp__eventmodelers__get_nodes { "boardId": "$BOARD_ID", "name": "<target>" }` — a partial, case-insensitive title match; prefer an exact title, and if several elements still match, list them and ask the user to pick one
46
+ - `target` is a cell name (e.g. `B3`) → `mcp__eventmodelers__get_nodes { "boardId": "$BOARD_ID", "chapterId": "$CHAPTER_ID" }` and take the node whose `cellName` is `target` (if multiple chapters exist, find the chapter first with `get_nodes { "type": "CHAPTER" }` and ask the user if ambiguous). Never construct a cell address yourself — only use one you were given or read back as a node's `cellName`.
47
47
 
48
- ```
49
- mcp__eventmodelers__add_field_examples { "boardId": "$BOARD_ID", "nodeId": "<target, if a UUID>" }
50
- ```
51
- ```
52
- mcp__eventmodelers__add_field_examples { "boardId": "$BOARD_ID", "name": "<target, if a name>" }
53
- ```
54
- ```
55
- mcp__eventmodelers__add_field_examples { "boardId": "$BOARD_ID", "cellName": "<target, if a cell name>", "timelineId": "$CHAPTER_ID" }
56
- ```
57
-
58
- If this succeeds, skip straight to Step 6 (report back), describing the fields the tool reports as changed. Use the manual fallback flow below (Steps 2–5) only if MCP isn't connected.
48
+ Then continue with Step 3 (linked elements) — `mcp__eventmodelers__get_connected_nodes { "boardId": "$BOARD_ID", "nodeId": "<resolved id>", "includeFields": true }` returns the neighbours and their fields in one call.
59
49
 
60
50
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 2 — Resolve the element".
61
51
 
@@ -15,11 +15,12 @@ curl -s "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/$target" \
15
15
  ```
16
16
 
17
17
  ### 2b — Name search
18
- If `target` is not a UUID, search by name:
18
+ If `target` is not a UUID, list the board's nodes and match on `meta.title` (the REST nodes endpoint has no name filter):
19
19
 
20
20
  ```bash
21
- curl -s "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/events/search?name=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$target")" \
22
- -H "x-user-id: examples-skill"
21
+ curl -s "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes" \
22
+ -H "x-user-id: examples-skill" \
23
+ | jq --arg t "$target" '[.[] | select((.meta.title // "") | ascii_downcase | contains($t | ascii_downcase))]'
23
24
  ```
24
25
 
25
26
  Pick the best match (exact title match preferred; case-insensitive). If multiple matches exist, list them and ask the user to pick one.
@@ -54,6 +54,7 @@ Write normal, full-size HTML/CSS for each page — as if designing a real webpag
54
54
  Guidelines:
55
55
  - **Keep it simple — this is a view-only mockup, not a working app.** No JavaScript, no interactivity beyond what a static page can show (enforced server-side, see below). Keep the CSS compact: inline styles or a handful of Bulma classes are enough — don't write a large embedded `<style>` block or reinvent what Bulma already gives you for free.
56
56
  - Each page is one complete, standalone HTML fragment — not a `data-step` div nested inside a shared blob. A multi-step flow (e.g. cart → payment → confirmation) is three separate pages in the array, each fully self-contained.
57
+ - **Each page may be at most 6000 characters** (the full markup string, including inline styles). The server rejects a longer page with `400 HTML_SCREEN_PAGE_TOO_LONG` (`context: {pageIndex, length, maxLength}`) and saves nothing — the render call fails as a whole. Check each page's length before rendering; if one is over, trim it (fewer inline styles, Bulma classes instead of custom CSS, fewer repeated sample rows) or split the content into another page. Adding a mark also counts toward the limit, since it adds attributes to the page's markup.
57
58
  - Inline styles (`style="..."`) are the simplest way to keep each page self-contained.
58
59
  - No `<script>` tags, no inline event handlers (`onclick`, `onload`, ...), no `javascript:` URIs — these are stripped server-side from every page before persisting regardless of what's sent. This is a static visual mockup, not an interactive prototype.
59
60
  - A real page background (e.g. a light gray full-bleed background behind a centered white card) reads more realistically than a bare form floating on white.
@@ -149,7 +150,7 @@ mcp__eventmodelers__create_screen {
149
150
 
150
151
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 4 — Creating a new node".
151
152
 
152
- Expect `204 No Content` on success from either curl call.
153
+ Expect `204 No Content` on success from either curl call. A `HTML_SCREEN_PAGE_TOO_LONG` error means a page exceeds 6000 characters — shorten or split that page (see Step 3's guidelines) and send the call again; don't give up on the render.
153
154
 
154
155
  ## Step 5 — Define field data lineage (mandatory)
155
156
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  Only needed when MCP is not connected. Every call below has an MCP equivalent in the main SKILL.md — always prefer that.
4
4
 
5
+ Each entry in `pages` may be at most 6000 characters — a longer page returns `400 {"code": "HTML_SCREEN_PAGE_TOO_LONG", "context": {"pageIndex", "length", "maxLength"}}` and nothing is saved.
6
+
5
7
  ## Step 2 — Load existing pages
6
8
 
7
9
  ```bash
@@ -43,7 +43,7 @@ Then **create one task per screen** using TaskCreate, naming each task after the
43
43
 
44
44
  ## HTML page design (default)
45
45
 
46
- Design each page as real, full-size HTML/CSS following the `html-screen` skill's conventions: full-size markup (16px body text, generous padding — the canvas scales it down, don't shrink it yourself), one complete self-contained fragment per page (no `<html>`/`<head>`/`<body>` wrapper), no `<script>`/inline handlers (stripped server-side), and Bulma CSS classes (`title`, `button`, `is-primary`, `field`/`control`/`input`, etc. — remember heading size modifiers like `class="title is-1"`) since Bulma 0.9.4 is loaded by default.
46
+ Design each page as real, full-size HTML/CSS following the `html-screen` skill's conventions: full-size markup (16px body text, generous padding — the canvas scales it down, don't shrink it yourself), one complete self-contained fragment per page (no `<html>`/`<head>`/`<body>` wrapper), no `<script>`/inline handlers (stripped server-side), and Bulma CSS classes (`title`, `button`, `is-primary`, `field`/`control`/`input`, etc. — remember heading size modifiers like `class="title is-1"`) since Bulma 0.9.4 is loaded by default. Keep every page at most 6000 characters per page (the server rejects a longer page with `400 HTML_SCREEN_PAGE_TOO_LONG` — split the screen into more pages or trim the markup instead).
47
47
 
48
48
  ## Sketch path (explicit request only)
49
49
 
@@ -53,10 +53,7 @@ Steps:
53
53
  chain they belong to, the timeline around them — and spend a full-`meta` `get_nodes` only
54
54
  on the handful you conclude you are actually going to touch. One `get_node` per changed
55
55
  node, or a second outline call for a chapter you already read this turn, is the same
56
- fetch paid for twice (see `connect` Step 5).
57
- `mcp__eventmodelers__get_board_events` with the header's `seq` range tells you what the
58
- change actually was when the node's current state doesn't make it obvious. Then judge the
59
- board as a whole: run `/analyze-existing-model` once per session to get that picture and
56
+ fetch paid for twice (see `connect` Step 5). Then judge the board as a whole: run `/analyze-existing-model` once per session to get that picture and
60
57
  keep it in mind across turns, refreshing it when a turn's changes invalidate it. On a
61
58
  `BOARD_REVIEW` turn that model-wide picture *is* the starting point.
62
59
  **What you read here is what you hand down in step 3.** Index it and keep it: every fact