@eventmodelers/cli 1.0.75 → 1.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +2 -0
  2. package/cli.js +41 -14
  3. package/package.json +1 -1
  4. package/shared/build-kit/README.md +38 -0
  5. package/shared/build-kit/ralph-exec.js +92 -0
  6. package/shared/skills/learn-eventmodelers-api/SKILL.md +118 -44
  7. package/shared/skills/update-slice-status/SKILL.md +8 -0
  8. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
  9. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
  10. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
  11. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +2 -0
  12. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/references/api-fallback.md +2 -1
  13. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/references/api-fallback.md +1 -0
  14. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +11 -1
  15. package/stacks/modeling-kit/templates/.claude/skills/html-screen/references/api-fallback.md +1 -1
  16. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +10 -3
  17. package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md +2 -0
  18. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +2 -0
  19. package/stacks/modeling-kit/templates/.claude/skills/storyboard/references/api-fallback.md +2 -0
  20. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +1 -1
  21. package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
  22. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
  23. package/stacks/react/templates/build-kit/README.md +38 -0
  24. package/stacks/react/templates/build-kit/lib/prompt.md +1 -1
  25. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
  26. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -1
  27. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
package/README.md CHANGED
@@ -79,6 +79,7 @@ your-project/
79
79
  ├── .build-kit/ ← agent runner (name is .agent-modeling-kit/ for the modeling-kit stack)
80
80
  │ ├── ralph-claude.js ← realtime agent + task loop
81
81
  │ ├── ralph-local-ai.js ← same, via a local/self-hosted model
82
+ │ ├── ralph-exec.js ← same, via an external agent command (Codex CLI, OpenCode, …)
82
83
  │ ├── ralph.sh ← bash-only loop (no realtime)
83
84
  │ ├── lib/ ← stack-specific agent prompts + helpers
84
85
  │ └── .slices/ ← board slices, written by `fetch`/`listen` (or pre-seeded by `init --demo`)
@@ -125,6 +126,7 @@ npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made de
125
126
  npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
126
127
  npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
127
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
130
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
129
131
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
130
132
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
package/cli.js CHANGED
@@ -31,7 +31,7 @@ const __dirname = dirname(__filename);
31
31
 
32
32
  // Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
33
33
  // Stacks with useShared:true also get shared/build-kit/* copied into their kit dir
34
- // first (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph.sh, realtime-agent.js,
34
+ // first (ralph.js, ralph-claude.js, ralph-local-ai.js, ralph-exec.js, ralph.sh, realtime-agent.js,
35
35
  // code-export.mjs, lib/agent.sh, lib/local-ai-agent.js, package.json, README.md) —
36
36
  // those files have no per-stack content, so they live once instead of being
37
37
  // copy-pasted into every stack (that copy-pasting is exactly how they drifted out
@@ -222,9 +222,11 @@ const MCP_MANUAL_CLIENTS = [
222
222
  // directory tells it to go read the canonical .claude/skills/<name>/SKILL.md and
223
223
  // follow it — the same pattern spec-kitty uses (verified directly against its repo,
224
224
  // not just its docs: every "stub" host below reads plain Markdown, no per-host
225
- // format transform needed). Codex CLI, Mistral Vibe, Pi, and Letta Code share one
226
- // convention that already matches our native SKILL.md format, so those get the
227
- // real file copied as-is instead of a stub.
225
+ // format transform needed). Codex CLI, Mistral Vibe, Pi, Letta Code, and Hermes
226
+ // share one convention that already matches our native SKILL.md format, so those
227
+ // get the real file copied as-is instead of a stub. (Hermes also has a native
228
+ // .hermes/skills/ location, but its docs list .agents/skills/ as an equally
229
+ // first-class project skill root, so there's no reason to write the tree twice.)
228
230
  const AGENT_HOSTS = {
229
231
  cursor: { label: 'Cursor', dir: '.cursor/commands', kind: 'stub' },
230
232
  windsurf: { label: 'Windsurf', dir: '.windsurf/workflows', kind: 'stub' },
@@ -238,12 +240,33 @@ const AGENT_HOSTS = {
238
240
  augment: { label: 'Augment Code', dir: '.augment/commands', kind: 'stub' },
239
241
  antigravity: { label: 'Google Antigravity', dir: '.agent/workflows', kind: 'stub' },
240
242
  codex: {
241
- label: 'Codex CLI / Mistral Vibe / Pi / Letta Code (shared .agents/skills/ convention)',
243
+ label: 'Codex CLI / Mistral Vibe / Pi / Letta Code / Hermes (shared .agents/skills/ convention)',
242
244
  dir: '.agents/skills',
243
245
  kind: 'skill-package',
244
246
  },
245
247
  };
246
248
 
249
+ // Several hosts share one directory convention, so they resolve to a single
250
+ // canonical entry above instead of each getting their own: writing the same
251
+ // .agents/skills/ tree four times would be pure duplication, and `--hosts all`
252
+ // would print four "installed" lines for one install. These exist so people can
253
+ // name the agent they actually use — `--hosts pi` is a friendlier spelling of
254
+ // `--hosts codex`, not a different install.
255
+ const AGENT_HOST_ALIASES = {
256
+ pi: 'codex',
257
+ hermes: 'codex',
258
+ vibe: 'codex',
259
+ 'mistral-vibe': 'codex',
260
+ letta: 'codex',
261
+ 'letta-code': 'codex',
262
+ };
263
+
264
+ // Alias -> canonical, deduped and order-preserving. Unknown keys pass through
265
+ // untouched so the caller keeps reporting them as unknown.
266
+ function resolveAgentHostKeys(keys) {
267
+ return [...new Set(keys.map((k) => AGENT_HOST_ALIASES[k] || k))];
268
+ }
269
+
247
270
  function agentHostStub(skillName) {
248
271
  return `# ${skillName} (eventmodelers)\n\nThis host should read the canonical skill at:\n\n**\`.claude/skills/${skillName}/SKILL.md\`**\n\nFollow those instructions when this command is invoked.\n`;
249
272
  }
@@ -269,15 +292,18 @@ async function configureAgentHosts({ hosts, global: useGlobal } = {}) {
269
292
  if (!hostKeys || !hostKeys.length) {
270
293
  console.log('\nAvailable agent hosts:');
271
294
  Object.entries(AGENT_HOSTS).forEach(([key, h]) => console.log(` ${key.padEnd(12)} ${h.label}`));
295
+ const aliasKeys = Object.keys(AGENT_HOST_ALIASES);
296
+ if (aliasKeys.length) console.log(`\n (also accepted: ${aliasKeys.join(', ')})`);
272
297
  const answer = await prompt('\nWhich hosts? (comma-separated keys, or "all"): ');
273
298
  hostKeys = answer.trim() === 'all'
274
299
  ? Object.keys(AGENT_HOSTS)
275
300
  : answer.split(',').map((s) => s.trim()).filter(Boolean);
276
301
  }
302
+ hostKeys = resolveAgentHostKeys(hostKeys);
277
303
 
278
304
  const unknown = hostKeys.filter((k) => !AGENT_HOSTS[k]);
279
305
  if (unknown.length) {
280
- console.error(`❌ Unknown host(s): ${unknown.join(', ')}. Available: ${Object.keys(AGENT_HOSTS).join(', ')}`);
306
+ console.error(`❌ Unknown host(s): ${unknown.join(', ')}. Available: ${Object.keys(AGENT_HOSTS).join(', ')} (aliases: ${Object.keys(AGENT_HOST_ALIASES).join(', ')})`);
281
307
  process.exit(1);
282
308
  }
283
309
  if (!hostKeys.length) {
@@ -1011,7 +1037,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
1011
1037
  }
1012
1038
 
1013
1039
  // Make scripts executable
1014
- for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js']) {
1040
+ for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
1015
1041
  const p = join(kitDir, script);
1016
1042
  if (existsSync(p)) {
1017
1043
  try { execSync(`chmod +x "${p}"`); } catch {}
@@ -2821,7 +2847,7 @@ program
2821
2847
  program
2822
2848
  .command('init-agents')
2823
2849
  .description(`Expose installed skills to other AI agent hosts (${Object.keys(AGENT_HOSTS).join(', ')}) as thin stub commands pointing at the canonical .claude/skills/ files — no skill content duplicated per host`)
2824
- .option('--hosts <list>', `Comma-separated host keys (${Object.keys(AGENT_HOSTS).join(', ')})`)
2850
+ .option('--hosts <list>', `Comma-separated host keys (${Object.keys(AGENT_HOSTS).join(', ')}; aliases: ${Object.keys(AGENT_HOST_ALIASES).join(', ')})`)
2825
2851
  .option('--all', 'Expose to every known host')
2826
2852
  .option('--global', 'Read skills from ~/.claude/skills/ instead of the project')
2827
2853
  .action(async (opts) => {
@@ -3041,6 +3067,7 @@ credentialFlags(program
3041
3067
  .command('run')
3042
3068
  .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')
3043
3069
  .option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner, via ralph-local-ai.js (build-kit stacks only). 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.`)
3070
+ .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.')
3044
3071
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3045
3072
  .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.')
3046
3073
  .option('--standalone', 'Let the modeling agent work the board in the background, on its own initiative: on top of direct prompts it subscribes to the board\'s change channel (like the build agents do) and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for. Changed nodes are a notification, not the task: it judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question comment). Filling that detail in while the human keeps modeling is the point — it does not wait for the board to be finished. Implies --modeling.')
@@ -3109,8 +3136,8 @@ credentialFlags(program
3109
3136
  // no meaning for a build kit, which is scaffolded per project by definition.
3110
3137
  if (opts.modeling || opts.standalone || opts.global) {
3111
3138
  const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : '--global';
3112
- if (opts.bash || opts.localAi) {
3113
- console.error(`❌ ${picked} is mutually exclusive with --bash/--local-ai — those select a build-kit runner, which the modeling loop has no use for.`);
3139
+ if (opts.bash || opts.localAi || opts.exec) {
3140
+ console.error(`❌ ${picked} is mutually exclusive with --bash/--local-ai/--exec — those select a build-kit runner, which the modeling loop has no use for.`);
3114
3141
  process.exit(1);
3115
3142
  }
3116
3143
  if (opts.local) {
@@ -3169,9 +3196,9 @@ credentialFlags(program
3169
3196
  const kitDir = buildKitDir;
3170
3197
 
3171
3198
  const localAiTarget = resolveLocalAiTarget(opts);
3172
- const pickedCount = [opts.bash, localAiTarget !== null].filter(Boolean).length;
3199
+ const pickedCount = [opts.bash, localAiTarget !== null, !!opts.exec].filter(Boolean).length;
3173
3200
  if (pickedCount > 1) {
3174
- console.error('❌ --bash and --local-ai are mutually exclusive — pick one.');
3201
+ console.error('❌ --bash, --local-ai and --exec are mutually exclusive — pick one runner.');
3175
3202
  process.exit(1);
3176
3203
  }
3177
3204
 
@@ -3179,7 +3206,7 @@ credentialFlags(program
3179
3206
  // is just a thin dispatcher so users don't have to remember the kit-dir name or which
3180
3207
  // runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
3181
3208
  // files freely; `run` always executes whatever is currently on disk.
3182
- const runner = opts.bash ? 'ralph.sh' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
3209
+ const runner = opts.bash ? 'ralph.sh' : opts.exec ? 'ralph-exec.js' : localAiTarget !== null ? 'ralph-local-ai.js' : 'ralph-claude.js';
3183
3210
  const runnerPath = join(kitDir, runner);
3184
3211
  if (!existsSync(runnerPath)) {
3185
3212
  console.error(`❌ ${relative(cwd, runnerPath)} not found.`);
@@ -3195,7 +3222,7 @@ credentialFlags(program
3195
3222
  // local-only branch even when .eventmodelers/config.json has valid credentials.
3196
3223
  // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
3197
3224
  // they reach both node runners but not ralph.sh, which has no heartbeat to identify.
3198
- execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}) } });
3225
+ execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '', ...(typeof localAiTarget === 'string' ? { LOCAL_AI_TARGET: localAiTarget } : {}), ...(typeof opts.exec === 'string' ? { RALPH_EXEC_CMD: opts.exec } : {}) } });
3199
3226
  } catch (err) {
3200
3227
  process.exit(err.status || 1);
3201
3228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
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": {
@@ -26,6 +26,7 @@ node .build-kit/ralph-claude.js /path/to/project
26
26
  |------|---------|
27
27
  | `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
28
28
  | `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
29
+ | `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
29
30
  | `ralph.sh` | Shell-based loop — alternative to the JS entry points |
30
31
  | `realtime-agent.js` | Standalone realtime agent — only needed to run it in a separate terminal |
31
32
 
@@ -87,6 +88,43 @@ default here is raised rather than left to the server. On the `openai` dialect t
87
88
  equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
88
89
  llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
89
90
 
91
+
92
+ ## External agent commands (`--exec`)
93
+
94
+ Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
95
+ are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
96
+ already is one and only wants a prompt. They go through `ralph-exec.js` instead:
97
+
98
+ ```bash
99
+ npx @eventmodelers/cli run --exec "codex exec --full-auto"
100
+ npx @eventmodelers/cli run --exec "opencode run"
101
+
102
+ # …or persist it and use the bare flag
103
+ RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
104
+ ```
105
+
106
+ The prompt is appended to the command as one shell-quoted argument, and is also written
107
+ to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
108
+ child runs with the project dir as its cwd and inherits stdio — a harness owns its own
109
+ output format, so there is no condensed per-step logging here the way `ralph-claude.js`
110
+ has it.
111
+
112
+ Persist a default alongside the local-AI settings:
113
+
114
+ ```json
115
+ {
116
+ "localAi": {
117
+ "exec": "codex exec --full-auto"
118
+ }
119
+ }
120
+ ```
121
+
122
+ One caveat worth knowing before reaching for this: the kits' prompts assume Claude
123
+ Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
124
+ primitive, so `init-agents` puts the skill files where they can find them, but
125
+ `lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
126
+ a skill file rather than *invoke* it.
127
+
90
128
  ## Config
91
129
 
92
130
  Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ // Ralph loop handing each prompt to an arbitrary external agent command, instead
3
+ // of the default Claude runner.
4
+ //
5
+ // This is the escape hatch for agentic harnesses that bring their own tool loop —
6
+ // Codex CLI, OpenCode, Gemini CLI, and whatever comes next. They are NOT --local-ai
7
+ // targets: --local-ai supplies the agent loop (we load the MCP tools and drive the
8
+ // tool-call rounds), whereas a harness already is one and only wants a prompt. One
9
+ // generic spawn covers all of them, which beats a hand-written runner per vendor.
10
+ //
11
+ // The prompt is appended to the command as a single quoted argument (what most
12
+ // harnesses expect) and is also written to a temp file named by RALPH_PROMPT_FILE,
13
+ // for commands that would rather read it than take it on the command line.
14
+ //
15
+ // Usage: node ralph-exec.js [project_dir]
16
+ // RALPH_EXEC_CMD="codex exec --full-auto" node ralph-exec.js
17
+ // RALPH_EXEC_CMD="opencode run" node ralph-exec.js
18
+ // Or persist it as localAi.exec in .eventmodelers/config.json.
19
+
20
+ import { startRalph, loadLocalConfig } from './lib/ralph.js';
21
+ import { spawn } from 'child_process';
22
+ import { writeFileSync, mkdtempSync } from 'fs';
23
+ import { tmpdir } from 'os';
24
+ import { dirname, join, resolve } from 'path';
25
+ import { fileURLToPath } from 'url';
26
+
27
+ const kitDir = dirname(fileURLToPath(import.meta.url));
28
+ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir, '..');
29
+
30
+ const cfg = loadLocalConfig(kitDir);
31
+ const localOnly = process.env.RALPH_LOCAL === '1';
32
+ const execCmd = process.env.RALPH_EXEC_CMD || cfg.localAi?.exec;
33
+
34
+ if (!execCmd) {
35
+ console.error('[ralph-exec] No agent command configured.');
36
+ console.error(' Set one for this run: eventmodelers run --exec "codex exec --full-auto"');
37
+ console.error(' Or persist a default as localAi.exec in .eventmodelers/config.json');
38
+ process.exit(1);
39
+ }
40
+
41
+ // Same rule as ralph-claude.js: --local must mean zero board contact, so credentials
42
+ // never reach the child even when config.json has them.
43
+ const inlineHeader = !localOnly && cfg.boardId
44
+ ? `board=${cfg.boardId} token=${cfg.token} org=${cfg.organizationId} baseUrl=${cfg.baseUrl}\n\n`
45
+ : '';
46
+
47
+ const childEnv = {
48
+ ...process.env,
49
+ ...(cfg.token && !localOnly ? { EVENTMODELERS_TOKEN: cfg.token } : {}),
50
+ ...(cfg.agentId && !localOnly ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
51
+ };
52
+
53
+ const promptDir = mkdtempSync(join(tmpdir(), 'ralph-exec-'));
54
+
55
+ // POSIX single-quote escaping: close, insert an escaped quote, reopen. The prompt is
56
+ // multi-line Markdown with backticks and $ in it, so it cannot go in unquoted.
57
+ function shellQuote(s) {
58
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
59
+ }
60
+
61
+ console.log(`[ralph-exec] command: ${execCmd}`);
62
+
63
+ function runExec(prompt) {
64
+ return new Promise((resolvePromise, reject) => {
65
+ const full = inlineHeader + prompt;
66
+ const promptFile = join(promptDir, 'prompt.md');
67
+ writeFileSync(promptFile, full);
68
+
69
+ // stdio inherit: the harness owns its own output format, and there is no
70
+ // cross-harness stream schema to parse into the condensed per-step logging
71
+ // that ralph-claude.js does — so it goes straight through.
72
+ const proc = spawn(`${execCmd} ${shellQuote(full)}`, {
73
+ cwd: projectDir,
74
+ stdio: 'inherit',
75
+ shell: true,
76
+ env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
77
+ });
78
+ proc.on('close', (code) => (code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`))));
79
+ proc.on('error', reject);
80
+ });
81
+ }
82
+
83
+ startRalph({
84
+ kitDir,
85
+ projectDir,
86
+ onTask: runExec,
87
+ onPlannedSlice: runExec,
88
+ localOnly,
89
+ }).catch((err) => {
90
+ console.error('[ralph] Fatal:', err);
91
+ process.exit(1);
92
+ });
@@ -25,39 +25,40 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
25
25
  | `get_node_comments` | `boardId`, `nodeId` | List comments on a node | §1 `GET .../nodes/:nodeId/comments` |
26
26
  | `get_board_events` | `boardId` | All board events, in sequence | §1 `GET .../events` |
27
27
  | `search_board_events` | `boardId`, `name` | Search events by node name | §1 `GET .../events/search` |
28
- | `submit_node_events` | `boardId`, `events[]`, `autoConnect?`, `compact?` | Create/update nodes (raw `NodeChangeEvent`/edge events). `autoConnect: false` places freshly-created nodes without wiring them to their own/previous-column neighbors (avoids a stray nearest-left edge); `compact: true` returns `{persisted: <count>}` instead of the per-node hash map | §3 `POST .../nodes/events` |
28
+ | `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. `autoConnect: false` places freshly-created nodes without wiring them to their own/previous-column neighbors (avoids a stray nearest-left edge); `compact: true` returns `{persisted: <count>}` instead of the per-node hash map | §3 `POST .../nodes/events` |
29
29
  | `delete_node` | `boardId`, `nodeId` | Delete a node. Deleting a chapter (timeline) cascades — every node placed in one of its cells, plus any node parented to it (e.g. SLICE_BORDER), is deleted too, along with all their edges | (via `node:deleted` event, §3) |
30
- | `create_drawing` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... | Freehand canvas annotation (path/rect/text) — never placed in a cell | — (no REST equivalent; MCP-only) |
30
+ | `create_drawing` / `create_drawings` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... (plural: `drawings[]`) | Freehand canvas annotation (path/rect/text/sticky) — never placed in a cell. Use the plural form whenever an annotation is more than one stroke (a loop plus its arrows and label is one annotation, not three calls) | — (REST `POST .../drawing/draw` accepts a single drawing or an array) |
31
31
  | `find_nodes_in_drawing` | `boardId`, `drawingId` | Nodes fully contained inside a drawing's bounding box | — (no REST equivalent; MCP-only) |
32
- | `create_chapter` | `boardId`, `x?`, `y?` | Create a timeline. Omitting `x`/`y` auto-stacks it below the lowest existing chapter (by its *actual current* row-height total, not the height it was created with — safe even after `add_lane` growth), plus a fixed margin | §2 `POST .../chapters` |
32
+ | `create_chapter` | `boardId`, `x?`, `y?`, `title?`, `columns?`, `lanes?: [{type, label?, height?}]` | Create a timeline. Omitting `x`/`y` auto-stacks it below the lowest existing chapter (by its *actual current* row-height total, not the height it was created with — safe even after `add_lane` growth), plus a fixed margin. Pass `title` to name it, `columns` for a known column count, and `lanes` to create named lanes (or several of one type, e.g. one swimlane per context) — none of the three needs a follow-up call, and the lanes are sorted into the required order automatically. The response carries every `columnId` and lane id, so the chapter needn't be read back before placing into it | §2 `POST .../chapters` |
33
33
  | `get_chapter_bounds` | `boardId` | Absolute canvas bounding box `{id, title, x, y, width, height}` of every chapter on the board — width/height derived from each chapter's current row/column layout, not a guessed default. Use before picking explicit `x`/`y` for `create_chapter` (e.g. placing below the chapter with the largest `y + height`) to avoid overlapping one that grew since it was created | §2 `GET .../chapters/bounds` |
34
34
  | `add_column` | `boardId`, `timelineId`, `index?`, `beforeNodeId?`, `afterNodeId?`, `count?` | Add one or more columns in one call. `count` inserts that many contiguously starting at the insertion point (default 1). Position with at most one of `index` (0-based), `beforeNodeId`, or `afterNodeId` (resolves the index from where that already-placed node currently sits) — omit all three to append | §2 `POST .../timelines/:id/columns` |
35
35
  | `delete_column` | `boardId`, `timelineId`, `columnId` | Delete a column | §2 `DELETE .../columns/:columnId` |
36
- | `add_lane` | `boardId`, `timelineId`, `type`, `label?`, `index?` | Add a lane/row | §2 `POST .../timelines/:id/lanes` |
36
+ | `add_lane` / `add_lanes` | `boardId`, `timelineId`, `type`, `label?`, `index?`, `height?` (plural: `lanes[]`) | Add a lane/row. Use the plural form for more than one; for a chapter that doesn't exist yet, pass `lanes` to `create_chapter` instead — that needs no lane call at all | §2 `POST .../timelines/:id/lanes` (accepts an array too) |
37
37
  | `remove_lane` | `boardId`, `timelineId`, `rowId` | Remove a lane | — (extends §2; no direct REST route) |
38
38
  | `move_node_in_timeline` | `boardId`, `timelineId`, `movedNodeId`, `toCellId` | Move a placed node to another cell — its previous cell is automatically cleared | — (MCP-only convenience) |
39
39
  | `move_timeline_structure` | `boardId`, `timelineId`, `kind` (`'column'\|'lane'`), `id`, `toIndex` | Reorder a column or lane (row) — `kind` picks which `id` refers to | — (MCP-only convenience) |
40
40
  | `move_timeline_position` | `boardId`, `timelineId`, `x`, `y` | Move a chapter node on canvas | — (MCP-only convenience) |
41
41
  | `drop_node_to_cell` | `boardId`, `timelineId`, `cellId`, `nodeId`, `nodeType` | Place an existing node into a cell — if it was already placed elsewhere on this timeline, that cell is automatically cleared | §2 `POST .../cells/:cellId/drop` |
42
42
  | `clear_cell` | `boardId`, `timelineId`, `cellId` | Unassign the node from a cell without deleting it — the cell becomes empty and the node survives (unplaced); no-op if already empty. Use `delete_node` to remove the node entirely | — (MCP-only convenience) |
43
- | `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?}`) | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane | §5 `POST .../slices` |
44
- | `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column | §5 `POST .../slice-definitions` |
45
- | `place_element` | `boardId`, `timelineId`, `elementType`, `title`, `columnIndex?`, `compact?`, `autoConnect?` | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT. `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
43
+ | `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?, fields?}`), `status?` | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane. `fields` writes that node's attributes in the same call, and `status` gives the SLICE_BORDER its `sliceStatus` on creation — neither needs a follow-up write | §5 `POST .../slices` |
44
+ | `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `status?`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column. `status` sets its `sliceStatus` straight away instead of a follow-up `update_slice_status` | §5 `POST .../slice-definitions` |
45
+ | `place_element` / `place_elements` | `boardId`, `timelineId`, `elementType`, `title`, `fields?`, `lane?`, `columnIndex?`, `compact?`, `autoConnect?` (plural: `elements[]`) | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT. `fields` writes the element's attributes in the same call — don't follow a placement with a `submit_node_events` just to set them. The plural form places a whole slice's or column run's worth in one call, applied in order so each entry sees the columns the previous one added. `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
46
46
  | `list_slices` | `boardId` | List slices (id, title, status) | §8 `GET .../slicedata/slices` |
47
- | `update_slice_status` | `boardId`, `sliceId`, `newStatus` | Change a SLICE_BORDER's `sliceStatus` | (via `node:changed` event, §3) |
47
+ | `get_slice_rework` | `boardId`, `contextId` | How much each slice of one context has been reworked: changes, steps backwards, and reopens after Done, most reworked first, plus planning metrics over them. `contextId` is a MODEL_CONTEXT or a timeline (a timeline resolves to the context it belongs to). Always per context — there is no board-wide form | §8 `GET .../reporting/rework/contexts/:contextId` |
48
+ | `update_slice_status` | `boardId`, `newStatus`, plus exactly one of `sliceId` / `sliceTitle` / `columnId` | Change a SLICE_BORDER's `sliceStatus`. With a title or column id there is no need to call `list_slices` first; an ambiguous title comes back with its candidates. A slice being created takes its status from `create_slice`/`create_slice_definition` instead | — (via `node:changed` event, §3) |
48
49
  | `get_slice_data` | `boardId`, `contextName?`, `contextId?`, `sliceId?` | Full element graph for slices in a context | §8 `GET /slicedata` |
49
50
  | `get_spec_info` | `boardId`, `timelineId`, `elementTypes?` | EVENT/COMMAND/READMODEL nodes valid in GWT steps. Pass `elementTypes` (subset of `EVENT`/`COMMAND`/`READMODEL`) to avoid pulling the full element list when only one or two types are needed — filtered server-side, not just after a full fetch | §6 `GET .../spec-info` |
50
51
  | `get_board_outline` | `boardId`, `chapterId` | One chapter's structure, compact: per-column node lists (`{id, type, title, lane}`) + a flat edge list, no HTML pages / field bodies / meta. The cheap "what is where and how is it wired" read — prefer over `get_nodes` (no projection) for orientation checks | — (MCP-only convenience) |
51
52
  | `get_connected_nodes` | `boardId`, `nodeId`, `chapterId?`, `direction?` (`inbound`/`outbound`/`both`), `depth?`, `types?`, `includeFields?` | Neighbours of **one** node — what feeds it and what it feeds. Answers from a single anchor, unlike `get_attribute_chain` (which needs both ends of the chain as cell names up front). `depth` follows a whole chain; `types` filters the result only, never the traversal. Each neighbour carries `via`: `"edge"` for a real connection, `"layout"` when the node has none in that direction and the neighbour was inferred from the grid using auto-connect's own window (own column + adjacent one, forward-only pairs). Real edges always win. The `layout` fallback is what makes hand-built/imported chapters — which routinely carry **zero** edges — readable instead of falsely empty | — (MCP-only convenience) |
52
53
  | `validate_model` | `boardId`, `chapterId`, `checks?[]` | Server-side Event Modeling structural checklist over one chapter — compact `findings` only. Checks: unplaced nodes, backward arrows (with the todo-list `EVENT→READMODEL` exception), zero/multi-issuer commands, sourceless read models, two-screens-in-a-column, missing scenarios. Replaces the manual per-type `get_nodes` + `get_node projection=edges` validation pass | — (MCP-only convenience) |
53
- | `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node. `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
54
+ | `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node — created automatically, and a scenario `id` is generated when omitted. A given/when/then step may be addressed by `{title, type}` instead of a node id, resolved against that timeline, so no `get_spec_info` call is needed first (an ambiguous title is reported with its candidates). `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
54
55
  | `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node. Use whenever `eventmodeling-elaborating-scenarios`'s GWT-vs-storyline decision rule calls for one (e.g. a todo list's open→close lifecycle) — not only when a user explicitly names "storyline"; that skill's own per-read-model judgment is the trigger, this catalog entry isn't a stricter gate on top of it. `compact: true` suppresses the full storyline echo | §6 `POST .../storylines` |
55
56
  | `set_connection` | `boardId`, `source`, `target`, `action` (`'connect'\|'remove'`) | Add or remove a type-checked directed edge. Batch form `set_connections` takes `connections[]` (applied in order) plus `compact?` — `compact: true` returns a `{connected, existed, removed, notFound, failed, errors}` tally instead of one row per edge | — (via `edges` on §3 events) |
56
57
  | `auto_connect_node` | `boardId`, `nodeId` | Re-run auto-connect for a node | §3 `POST .../nodes/:nodeId/auto-connect` |
57
- | `link_element` | `boardId`, `nodeId`, `targetNodeId` | Link two existing same-type nodes: `targetNodeId` is replaced with a full copy of `nodeId`'s meta plus `meta.linkedTo`. Linking means first create, then link | §3 `POST .../nodes/:nodeId/link` |
58
- | `add_comment` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'`), `author?` | Add a comment — word the `text` as a question to flag gaps/edge cases during review; there is no separate `QUESTION` type | (via comment events) |
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` |
59
+ | `add_comment` / `add_comments` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'\|'QUESTION'`), `author?` (plural: `comments[]`, each with its own `nodeId`) | Add a comment — `QUESTION` is the type for a gap/edge case raised during review. Use the plural form for a review that has a question per element: all of them go in one call | §1 `POST .../boards/:boardId/comments` (batch) |
59
60
  | `update_comment` | `boardId`, `nodeId`, `commentId`, `action` (`'resolve'\|'delete'`) | Resolve or delete a comment | — (via comment events) |
60
- | `create_screen` | `boardId`, `contentType` (`'image'\|'sketch'\|'html'`), `nodeId?`, `chapterId`, `cellId?`/`cellName?`, plus content fields (`imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`), `description?`, `fields?`, `autoConnect?` | Create + place a new screen node (SCREEN or HTML_SCREEN) atomically, in one call. Batch form `create_screens` takes `screens[]` (HTML only) + `autoConnect?`. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
61
+ | `create_screen` | `boardId`, `contentType` (`'image'\|'sketch'\|'html'`), `nodeId?`, `chapterId`, `cellId?`/`cellName?`, plus content fields (`imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`), `title?`, `description?`, `fields?`, `autoConnect?` | Create + place a new screen node (SCREEN or HTML_SCREEN) atomically, in one call. `title` names the node (`meta.title`) in the same call — no follow-up `node:changed` just to label the screen; `create_screens` takes it per entry. Batch form `create_screens` takes `screens[]` (HTML only) + `autoConnect?`. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
61
62
  | `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` |
62
63
  | `add_field_examples` | `boardId`, `nodeId?`, `name?`, `cellName?`, `timelineId?` | Fill empty field examples using linked-node context | — (MCP-only convenience) |
63
64
  | `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source | — (MCP-only convenience) |
@@ -209,6 +210,24 @@ Get all comments for a node.
209
210
 
210
211
  ---
211
212
 
213
+ ### POST `/api/org/:orgId/boards/:boardId/nodes/:nodeId/comments`
214
+ Add one comment to one node.
215
+
216
+ **Request body**: `{ text: string, type?: 'COMMENT' | 'TASK' | 'QUESTION', author?: string }`
217
+ **Response**: `201` — `{ id }`
218
+
219
+ ---
220
+
221
+ ### POST `/api/org/:orgId/boards/:boardId/comments`
222
+ Post several comments, on any nodes of one board, in a single request — the batch form. Use it for a review that raises a question per element (`/wdyt`) instead of one request per comment.
223
+
224
+ **Request body**: `Array<{ nodeId: string, text: string, type?: 'COMMENT' | 'TASK' | 'QUESTION', author?: string }>`
225
+ **Response**: `201` — `{ results: Array<{ nodeId, id } | { nodeId, error }> }`, in request order
226
+
227
+ Entries are independent: one naming a node that doesn't exist is reported in `results` while the rest are still posted.
228
+
229
+ ---
230
+
212
231
  ### POST `/api/org/:orgId/boards/:boardId/bucket`
213
232
  Create a Supabase storage bucket for the board.
214
233
 
@@ -225,8 +244,19 @@ A "chapter" is a timeline — the same entity, referenced as `chapterId` in node
225
244
  ### POST `/api/org/:orgId/boards/:boardId/chapters`
226
245
  Create a chapter node.
227
246
 
228
- **Request body**: `{ position?: { x: number, y: number } }`
229
- **Response**: `200` — chapter data
247
+ **Request body**:
248
+ ```typescript
249
+ {
250
+ position?: { x: number, y: number }
251
+ title?: string // names the chapter on creation — no follow-up rename
252
+ columns?: number // initial column count, default 3
253
+ lanes?: Array<{ type: 'actor'|'interaction'|'swimlane'|'spec'|'feedback'|'table', label?: string, height?: number }>
254
+ }
255
+ ```
256
+
257
+ **Response**: `200` — `{ id, eventId, columnIds: string[], lanes: Array<{id, type, label}> }`
258
+
259
+ `lanes` replaces the default Actor/Interaction/Swimlane/Spec set — this is how a chapter gets lanes named after what they hold, or several lanes of one type (one swimlane per context), without an `add_lane` call per lane. They are sorted into the required order (actor → interaction → swimlane → spec → feedback/table) automatically, keeping the given order within a type, so no ordering error is possible. Every column id and lane id comes back in the response — don't re-read the chapter just to place something into it.
230
260
 
231
261
  Omitting `position` auto-stacks the new chapter below the lowest existing chapter on the board, using each existing chapter's *actual current* row-height total (not the height it was created with) plus a fixed margin — so a chapter that grew via `add_lane`/`add_column` after another was stacked below it won't get overlapped by yet another auto-stacked chapter.
232
262
 
@@ -258,18 +288,22 @@ Delete a column from a timeline. Removes the column and all its cells. Cannot de
258
288
  ---
259
289
 
260
290
  ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes`
261
- Add a lane (row) to a timeline.
291
+ Add one or more lanes (rows) to a timeline.
262
292
 
263
293
  **Request body**:
264
294
  ```typescript
265
295
  {
266
- type: 'actor' | 'interaction' | 'swimlane' | 'spec' | 'feedback'
296
+ type: 'actor' | 'interaction' | 'swimlane' | 'spec' | 'feedback' | 'table'
267
297
  label?: string
268
298
  index?: number
269
299
  height?: number
270
300
  }
301
+ // or, batch form — every lane added in one request, applied in order:
302
+ [ { type: 'interaction', label: 'Interaction' }, { type: 'swimlane', label: 'Ordering' } ]
271
303
  ```
272
- **Response**: `200` — lane data
304
+ **Response**: `200` — lane data, or `{ lanes: [...] }` for the batch form
305
+
306
+ For a chapter that doesn't exist yet, pass `lanes` to `POST .../chapters` instead — that needs no lane call at all.
273
307
 
274
308
  ---
275
309
 
@@ -434,16 +468,27 @@ Create a single type-checked directed edge between two existing nodes — the RE
434
468
  ---
435
469
 
436
470
  ### POST `/api/org/:orgId/boards/:boardId/nodes/:nodeId/link`
437
- Link two existing same-type nodes — the REST fallback for `link_element`. Linking means first create, then link: `targetNodeId` must already exist. It's replaced with a full copy of `:nodeId`'s meta (not a merge) plus `meta.linkedTo`. COMMAND/EVENT/READMODEL only; `:nodeId` must not itself already be a linked copy.
471
+ Turn a node into a linked copy of `:nodeId` — the REST fallback for `link_element`. The copy receives a full copy of `:nodeId`'s meta (a replacement, not a merge) plus `meta.linkedTo`. COMMAND/EVENT/READMODEL only; `:nodeId` must not itself already be a linked copy.
438
472
 
439
- **Request body**:
473
+ **Request body** — exactly one of the two:
440
474
  ```typescript
441
475
  {
442
476
  targetNodeId: string // existing same-type node to convert into a linked copy
443
477
  }
478
+ // or: create the copy in this same request
479
+ {
480
+ place: {
481
+ timelineId: string // chapter to place the copy on
482
+ columnIndex?: number // columns are added to reach it
483
+ lane?: string // row id or label, when the chapter has several lanes of that type
484
+ autoConnect?: boolean // default true
485
+ }
486
+ }
444
487
  ```
445
488
 
446
- **Response**: `200` `{ nodeId, linkedTo, type }` · `400` — missing `targetNodeId`, type mismatch, self-link, unsupported element type, or the original is itself a linked copy · `404` the original or `targetNodeId` doesn't exist
489
+ With `place` the copy inherits the original's element type and title, so a translation or automation chain no longer needs a `node:created` call before this one.
490
+
491
+ **Response**: `200` — `{ nodeId, linkedTo, type }`, plus `placed: { nodeId, cellName, columnIndex }` when `place` was used · `400` — neither or both of `targetNodeId`/`place`, type mismatch, self-link, unsupported element type, or the original is itself a linked copy · `404` — the original or `targetNodeId` doesn't exist
447
492
 
448
493
  ---
449
494
 
@@ -470,7 +515,7 @@ Update an image snapshot.
470
515
  ### POST `/api/org/:orgId/boards/:boardId/image-nodes/:nodeId`
471
516
  Create an image node.
472
517
 
473
- **Request**: `multipart/form-data` — fields: `file`, `chapterId`, `cellName`
518
+ **Request**: `multipart/form-data` — fields: `file`, `chapterId`, `cellName`, `title?` (label shown on the node, `meta.title`)
474
519
  **Response**: `204`
475
520
 
476
521
  ---
@@ -499,6 +544,7 @@ Create a SCREEN node from a sketch description.
499
544
  cellName: string
500
545
  description: { elements: object[] }
501
546
  semanticDescription?: string
547
+ title?: string // label shown on the node (meta.title)
502
548
  }
503
549
  ```
504
550
  **Response**: `204` OR `400` (validation error)
@@ -518,13 +564,16 @@ Create a complete slice (1 column + its nodes automatically placed).
518
564
  type: 'state-change' | 'state-view' | 'automation'
519
565
  index?: number
520
566
  nodes?: {
521
- actor?: Partial<NodeData> & { rowId?: string }
522
- interaction?: Partial<NodeData> & { rowId?: string }
523
- swimlane?: Partial<NodeData> & { rowId?: string }
567
+ actor?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
568
+ interaction?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
569
+ swimlane?: Partial<NodeData> & { rowId?: string, fields?: FieldDef[] }
524
570
  }
571
+ status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
525
572
  }
526
573
  ```
527
574
 
575
+ `fields` writes that node's attributes in the same call, and `status` gives the SLICE_BORDER its `sliceStatus` on creation — neither needs a follow-up write.
576
+
528
577
  **Slice node mapping**:
529
578
  - `state-change` → HTML_SCREEN (actor) + COMMAND (interaction) + EVENT (swimlane)
530
579
  - `state-view` → HTML_SCREEN (actor) + READMODEL (interaction) + EVENT (swimlane, **only when `nodes.swimlane` is passed**)
@@ -546,12 +595,13 @@ Create a standalone SLICE_BORDER node spanning an **existing** column. Unlike th
546
595
  {
547
596
  columnId: string // id of an existing column on this timeline
548
597
  title: string // slice title — always taken from this field, never derived
598
+ status?: 'Created' | 'Planned' | 'InProgress' | 'Review' | 'Done' | 'Blocked' | 'Assigned' | 'Informational'
549
599
  data?: Record<string, unknown> // optional node.data payload
550
600
  meta?: Record<string, unknown> // optional extra meta fields (type, colId, title are always set explicitly and cannot be overridden here)
551
601
  }
552
602
  ```
553
603
 
554
- **Response**: `200` — `{ nodeId, timelineId, columnId, title }`
604
+ **Response**: `200` — `{ nodeId, timelineId, columnId, title, status? }` — `status` sets `sliceStatus` straight away, instead of a follow-up `update_slice_status`
555
605
  **Errors**: `400` missing `columnId`/`title` or column not found · `404` timeline not found
556
606
 
557
607
  ---
@@ -560,47 +610,47 @@ Create a standalone SLICE_BORDER node spanning an **existing** column. Unlike th
560
610
 
561
611
  **File**: `src/slices/change/api-specs/routes.ts`
562
612
 
563
- ### POST `/api/org/:orgId/boards/:boardId/contexts/:contextName/slices/:sliceName/scenarios`
564
- Append a Given-When-Then scenario to a spec node.
613
+ ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId/scenarios`
614
+ Append Given-When-Then scenario(s) to a column's spec node — the node is created automatically if the spec cell is empty. Accepts a single scenario object or an array.
565
615
 
566
616
  **Request body**:
567
617
  ```typescript
568
618
  {
569
- id: string
619
+ id?: string // generated when omitted
570
620
  title: string
571
621
  vertical?: boolean
572
622
  examples?: unknown[]
573
- given: string[] // nodeIds — must be EVENTs from same timeline
574
- when: string[] // nodeIds — at most one COMMAND; empty if then has READMODEL
575
- then: string[] // nodeIds — EVENTs only OR exactly one READMODEL (not mixed)
623
+ given?: SpecStep[] // EVENTs from this timeline
624
+ when?: SpecStep[] // at most one COMMAND, or one inline QUERY object; empty if then has a READMODEL
625
+ then?: SpecStep[] // EVENTs only OR exactly one READMODEL (not mixed)
626
+ expectError?: boolean, errorDescription?: string // error case: leave `then` empty
576
627
  }
628
+
629
+ // A step is addressed either way:
630
+ { id: 'node-uuid' } // a board node
631
+ { title: 'OrderPlaced', type: 'EVENT' } // resolved against this timeline's own elements
577
632
  ```
578
633
 
634
+ A step given as `{title, type}` is resolved server-side, so the names you already know need **no `get_spec_info`/`get_nodes` call first**. A title matching more than one element is rejected (`SCENARIO_ITEM_AMBIGUOUS`) with the candidates rather than guessed at, and resolution runs before anything is written — a bad name leaves no empty spec node behind.
635
+
579
636
  **Validation rules**:
580
637
  - `given`: only EVENTs from same timeline
581
- - `when`: max one COMMAND; must be empty when `then` contains a READMODEL
638
+ - `when`: max one COMMAND; must be empty when `then` contains a READMODEL (use an inline QUERY item for a state-view)
582
639
  - `then`: all EVENTs OR exactly one READMODEL — never mixed
583
640
  - All referenced nodes must belong to the same chapter/timeline
584
641
 
585
642
  **Response**:
586
- - `201` — `{ scenario, scenarios, specNodeId, isNewNode: boolean }`
643
+ - `201` — `{ specNodeId, scenarios, added, isNewNode: boolean }`
587
644
  - `400` — validation error
588
- - `404` — context or slice not found
645
+ - `404` — timeline, column, or a referenced node not found
589
646
  - `409` — duplicate scenario title
590
647
 
591
648
  ---
592
649
 
593
- ### GET `/api/org/:orgId/boards/:boardId/contexts/:contextName/spec-info`
594
- Get valid elements for a context (by name lookup).
595
-
596
- **Response**: `200` — `{ chapterId: string, elements: ElementRecord[] }`
597
-
598
- ---
599
-
600
- ### GET `/api/org/:orgId/boards/:boardId/contexts/:contextName/slices/:sliceName/spec-info`
601
- Get valid elements for a specific slice.
650
+ ### GET `/api/org/:orgId/boards/:boardId/timelines/:timelineId/spec-info`
651
+ Get the elements a scenario's given/when/then steps may legally reference (EVENT, COMMAND, READMODEL of that timeline). Only needed when you want the ids themselves — a scenario step can name its element by title instead.
602
652
 
603
- **Response**: `200` — `{ chapterId: string, elements: ElementRecord[] }`
653
+ **Response**: `200` — `{ timelineId: string, elements: ElementRecord[] }`
604
654
 
605
655
  ---
606
656
 
@@ -682,6 +732,30 @@ List all slices on a board.
682
732
 
683
733
  ---
684
734
 
735
+ ### GET `/api/org/:orgId/boards/:boardId/reporting/rework/contexts/:contextId`
736
+ **File**: `src/slices/change/reporting/rework/routes.ts`
737
+
738
+ How much each slice of one context has been reworked — read-only, derived from the board event log; nothing is stored
739
+ for it.
740
+
741
+ A slice's rework is read from its own SLICE_BORDER history. Going backwards means a step down the progress order
742
+ (Created → Planned → Assigned → InProgress → Review → Done), or reaching `Blocked` from `Review`/`Done`;
743
+ `Informational` is never scored. `reopens` counts transitions leading away from `Done`, `changes` counts writes to the
744
+ slice itself. This says nothing about edits to the elements inside a slice.
745
+
746
+ `contextId` is a MODEL_CONTEXT node id, or a timeline id — a timeline resolves through the board's own effective
747
+ context (the same rule `slicedata` uses), so one that inherits a context is reported under that context and one with
748
+ none is its own. Any other node type is rejected. Always scoped to one context; there is no board-wide form.
749
+
750
+ **Response**: `200` — `{ contextId, contextName, metrics, slices[] }`, slices most reworked first.
751
+ `slices[]` = `{ sliceId, title, status, changes, reopens, regressions, everReachedDone }`.
752
+ `metrics` = `{ slices, everReachedDone, reopenedAfterDone, firstTimeRightRate, currentlyReopened, avgReopensPerSlice,
753
+ avgChangesPerSlice }` — `firstTimeRightRate` is the share of the slices that reached Done and never came back, or
754
+ `null` when none got there yet.
755
+ `400` `CONTEXT_ID_REQUIRED` / `CONTEXT_NODE_INVALID` · `404` `CONTEXT_NOT_FOUND`
756
+
757
+ ---
758
+
685
759
  ## 9. Extensions
686
760
 
687
761
  **File**: `src/slices/extensions/routes.ts`
@@ -71,6 +71,14 @@ Prefer the MCP tool — it does the same `node:changed`/`sliceStatus` update in
71
71
  mcp__eventmodelers__update_slice_status { "boardId": "<BOARD_ID>", "sliceId": "<SLICE_NODE_ID>", "newStatus": "<newStatus>" }
72
72
  ```
73
73
 
74
+ The tool also resolves the slice itself — pass `sliceTitle` (or `columnId`) instead of `sliceId` and the listing step above can be skipped entirely; an ambiguous title comes back with its candidates instead of a guess:
75
+
76
+ ```
77
+ mcp__eventmodelers__update_slice_status { "boardId": "<BOARD_ID>", "sliceTitle": "Place Order", "newStatus": "<newStatus>" }
78
+ ```
79
+
80
+ A slice that is being *created* takes its status straight from `create_slice`/`create_slice_definition` (`status`) — don't create it and then call this.
81
+
74
82
  **Fallback (no MCP)** — send a `node:changed` event to update the `sliceStatus` field in the SLICE_BORDER node's meta directly:
75
83
 
76
84
  ```bash
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -309,6 +309,7 @@ mcp__eventmodelers__create_screen {
309
309
  "nodeId": "<SCREEN_NODE_ID>",
310
310
  "chapterId": "<CHAPTER_ID>",
311
311
  "cellId": "<CELL_ID>",
312
+ "title": "<screen.title>",
312
313
  "pages": ["<reconstructed HTML fragment for this screen>"],
313
314
  "description": "<screen.description — 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
314
315
  }
@@ -325,6 +326,7 @@ mcp__eventmodelers__create_screen {
325
326
  "nodeId": "<SCREEN_NODE_ID>",
326
327
  "chapterId": "<CHAPTER_ID>",
327
328
  "cellName": "<CELL_NAME>",
329
+ "title": "<screen.title>",
328
330
  "imageBase64": "<base64-encoded contents of screen.filepath, no data: URI prefix>",
329
331
  "mimeType": "image/png",
330
332
  "description": "<screen.description — 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
@@ -58,6 +58,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/image-nodes/$SCREEN_
58
58
  -H "x-token: $TOKEN" \
59
59
  -F "file=@<screen.filepath>" \
60
60
  -F "chapterId=$CHAPTER_ID" \
61
- -F "cellName=$CELL_NAME"
61
+ -F "cellName=$CELL_NAME" \
62
+ -F "title=<screen.title>"
62
63
  ```
63
64
  For the HTML path with no MCP, use the `html-screen-nodes` endpoint per the `html-screen` skill's fallback mechanics instead.
@@ -36,6 +36,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/<n
36
36
  -d '{
37
37
  "chapterId": "<CHAPTER_ID>",
38
38
  "cellId": "<actorRowId>-<columnId>",
39
+ "title": "<Screen Title>",
39
40
  "pages": ["<div>...</div>"]
40
41
  }'
41
42
  ```
@@ -48,7 +48,17 @@ mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "nodeId": "$NODE_ID",
48
48
 
49
49
  **Fallback (no MCP):** see `references/api-fallback.md` — "Action: place".
50
50
 
51
- **Batching (when called in bulk, e.g. from `wdyt`):** send one request per comment there is no batch endpoint for comments. Fire them sequentially, not in a single payload.
51
+ **Batching (when called in bulk, e.g. from `wdyt`):** send them all in one request instead of one per comment each entry names its own node:
52
+
53
+ ```bash
54
+ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/comments" \
55
+ -H "Authorization: Bearer $TOKEN" \
56
+ -H "Content-Type: application/json" \
57
+ -d '[{"nodeId":"<id>","text":"<text>","type":"QUESTION","author":"wdyt"},
58
+ {"nodeId":"<id2>","text":"<text2>","type":"QUESTION","author":"wdyt"}]'
59
+ ```
60
+
61
+ Response: `201 {"results":[{"nodeId":"<id>","id":"<commentId>"}, …]}` in request order — an entry whose node doesn't exist reports `error` there without dropping the rest. Over MCP this is the `add_comments` tool.
52
62
 
53
63
  **Report:**
54
64
  ```
@@ -31,7 +31,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/$N
31
31
  -H "x-board-id: $BOARD_ID" \
32
32
  -H "x-user-id: agent" \
33
33
  -H "Content-Type: application/json" \
34
- -d '{"chapterId": "'"$CHAPTER_ID"'", "cellName": "'"$CELL_NAME"'", "pages": ["<div>...</div>"]}'
34
+ -d '{"chapterId": "'"$CHAPTER_ID"'", "cellName": "'"$CELL_NAME"'", "title": "<Screen Title>", "pages": ["<div>...</div>"]}'
35
35
  ```
36
36
 
37
37
  ## Step 5 — Define field data lineage
@@ -46,10 +46,13 @@ mcp__eventmodelers__place_element {
46
46
  "timelineId": "<TIMELINE_ID>",
47
47
  "elementType": "<COMMAND|READMODEL|EVENT>",
48
48
  "title": "<title>",
49
- "columnIndex": <position, if given>
49
+ "columnIndex": <position, if given>,
50
+ "fields": [{ "name": "orderId", "type": "String", "example": "ord-1" }]
50
51
  }
51
52
  ```
52
53
 
54
+ Pass `fields` whenever the element's attributes are already known — they are written by this same call, so don't follow a placement with a `submit_node_events` just to set them. Placing **more than one** element is `place_elements` with an `elements` array (same per-entry options), applied in order so each entry sees the columns the previous one added — one call for a whole slice's or column run's worth instead of one per element.
55
+
53
56
  This tool finds or creates an empty cell in the correct lane and places the node in one call — it collapses the "resolve timeline → fetch columns → determine lane → check occupancy → create node" sequence (Steps 2–3, 4, 6, 7b below) into a single round trip. A `columnIndex` past the timeline's current column count is handled automatically (columns are added to reach it) — no need to pre-check the column count or catch an out-of-range error yourself. If `timelineId` is unknown, resolve it first via Step 2's MCP call. Pass `compact: true` for a smaller `{nodeId, cellName, columnIndex}` response (plus `connectedCount` if auto-connect wired an edge) when you don't need the full `lane`/`elementType`/`title`/`autoConnected` detail back. Go straight to Step 8 once it returns.
54
57
 
55
58
  **This does not cover**: `SCREEN`/`AUTOMATION`/`SCENARIO` (see their dedicated steps below), the `"after <title>"` position form, or an explicit `cellName` fast path (Step 1) — `place_element` has no way to express either. For those cases, or when MCP isn't connected, fall through to the manual steps below.
@@ -246,12 +249,14 @@ If no matching row is found, stop and report the error — the timeline may be m
246
249
 
247
250
  **Connections only ever pair nodes on the same timeline** — a node in Chapter A can never be wired directly to a node in Chapter B, even for an otherwise-valid type pair (e.g. `EVENT → READMODEL`). If the element you're placing needs to connect to something that lives on a *different* timeline, do not place it and then attempt `set_connection`/auto-connect across timelines — it will fail.
248
251
 
249
- Instead, create a **linked copy**: place the new node normally (Step 7, same title/type as the origin), then call `link_element` to mark it as a copy of the origin node:
252
+ Instead, create a **linked copy**. `link_element` places it for you pass the timeline (and optionally the column/lane) instead of an already-placed `targetNodeId`, and it creates the copy with the origin's type and title and links it in the same call:
250
253
 
251
254
  ```
252
- mcp__eventmodelers__link_element { "boardId": "<BOARD_ID>", "nodeId": "<origin-node-id>", "targetNodeId": "<newly-placed-node-id>" }
255
+ mcp__eventmodelers__link_element { "boardId": "<BOARD_ID>", "nodeId": "<origin-node-id>", "timelineId": "<TIMELINE_ID>", "columnIndex": 4 }
253
256
  ```
254
257
 
258
+ (If the copy already exists — e.g. it was placed by an earlier step — pass `targetNodeId: "<existing-node-id>"` instead.)
259
+
255
260
  (REST fallback: see `references/api-fallback.md` — "Step 6a — Link a node to an origin on a different timeline".) This replaces the new node's meta with a full copy of the origin's, sets `meta.linkedTo`, and only works for COMMAND/EVENT/READMODEL. Once linked, wire the local copy to its neighbors with normal same-timeline `set_connection`/auto-connect calls. `eventmodeling-checking-completeness` treats any `linkedTo`-marked node it finds as this intentional pattern, never a duplicate to flag.
256
261
 
257
262
  ---
@@ -273,6 +278,7 @@ mcp__eventmodelers__create_screen {
273
278
  "nodeId": "<node-uuid>",
274
279
  "chapterId": "<TIMELINE_ID>",
275
280
  "cellId": "<CELL_ID>",
281
+ "title": "<title>",
276
282
  "pages": ["<div>...</div>"],
277
283
  "description": "<title — what this screen shows>"
278
284
  }
@@ -291,6 +297,7 @@ mcp__eventmodelers__create_screen {
291
297
  "nodeId": "<node-uuid>",
292
298
  "chapterId": "<TIMELINE_ID>",
293
299
  "cellId": "<CELL_ID>",
300
+ "title": "<title>",
294
301
  "elements": [...],
295
302
  "description": "<title — what this screen shows>"
296
303
  }
@@ -95,6 +95,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/<n
95
95
  -d '{
96
96
  "chapterId": "<TIMELINE_ID>",
97
97
  "cellId": "<CELL_ID>",
98
+ "title": "<title>",
98
99
  "pages": ["<div>...</div>"]
99
100
  }'
100
101
  ```
@@ -110,6 +111,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/image-nodes/<node-uu
110
111
  -d '{
111
112
  "chapterId": "<TIMELINE_ID>",
112
113
  "cellId": "<CELL_ID>",
114
+ "title": "<title>",
113
115
  "description": {"elements": [...]},
114
116
  "semanticDescription": "<title — what this screen shows>"
115
117
  }'
@@ -183,6 +183,7 @@ mcp__eventmodelers__create_screen {
183
183
  "nodeId": "<SCREEN_NODE_ID>",
184
184
  "chapterId": "<CHAPTER_ID>",
185
185
  "cellId": "<actorCellId>",
186
+ "title": "<screenTitle>",
186
187
  "pages": ["<div>...</div>"],
187
188
  "description": "<screenTitle — what this screen shows>"
188
189
  }
@@ -201,6 +202,7 @@ mcp__eventmodelers__create_screen {
201
202
  "nodeId": "<SCREEN_NODE_ID>",
202
203
  "chapterId": "<CHAPTER_ID>",
203
204
  "cellId": "<actorCellId>",
205
+ "title": "<screenTitle>",
204
206
  "elements": [...],
205
207
  "description": "<screenTitle — what this screen shows>"
206
208
  }
@@ -39,6 +39,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/$S
39
39
  -d '{
40
40
  "chapterId": "<CHAPTER_ID>",
41
41
  "cellId": "<actorCellId>",
42
+ "title": "<screenTitle>",
42
43
  "pages": ["<div>...</div>"]
43
44
  }'
44
45
  ```
@@ -54,6 +55,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/image-nodes/$SCREEN_
54
55
  -d '{
55
56
  "chapterId": "<CHAPTER_ID>",
56
57
  "cellId": "<actorCellId>",
58
+ "title": "<screenTitle>",
57
59
  "description": {"elements": [...]},
58
60
  "semanticDescription": "<screenTitle — what this screen shows>"
59
61
  }'
@@ -138,7 +138,7 @@ Use the `handle-comment` skill with `action=place` to post each comment. Pass:
138
138
  - `type` — `COMMENT` (there is no separate question type — the text itself carries the question)
139
139
  - `author` — `wdyt`
140
140
 
141
- The comment API has no batch endpoint `handle-comment` sends one request per comment. Fire them sequentially.
141
+ Post them together, not one at a time: `handle-comment` sends every comment of a run in a single batch request (`add_comments` over MCP, `POST .../boards/:boardId/comments` over REST).
142
142
 
143
143
  Only post questions that are **genuinely unclear or missing** — don't post observations that are clearly intentional design decisions.
144
144
 
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -32,6 +32,7 @@ node .build-kit/ralph-claude.js /path/to/project
32
32
  |------|---------|
33
33
  | `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
34
34
  | `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
35
+ | `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
35
36
  | `ralph.sh` | Shell-based loop — alternative to the JS entry points |
36
37
 
37
38
  **Internals** (`lib/`):
@@ -80,6 +81,43 @@ default here is raised rather than left to the server. On the `openai` dialect t
80
81
  equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
81
82
  llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
82
83
 
84
+
85
+ ## External agent commands (`--exec`)
86
+
87
+ Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
88
+ are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
89
+ already is one and only wants a prompt. They go through `ralph-exec.js` instead:
90
+
91
+ ```bash
92
+ npx @eventmodelers/cli run --exec "codex exec --full-auto"
93
+ npx @eventmodelers/cli run --exec "opencode run"
94
+
95
+ # …or persist it and use the bare flag
96
+ RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
97
+ ```
98
+
99
+ The prompt is appended to the command as one shell-quoted argument, and is also written
100
+ to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
101
+ child runs with the project dir as its cwd and inherits stdio — a harness owns its own
102
+ output format, so there is no condensed per-step logging here the way `ralph-claude.js`
103
+ has it.
104
+
105
+ Persist a default alongside the local-AI settings:
106
+
107
+ ```json
108
+ {
109
+ "localAi": {
110
+ "exec": "codex exec --full-auto"
111
+ }
112
+ }
113
+ ```
114
+
115
+ One caveat worth knowing before reaching for this: the kits' prompts assume Claude
116
+ Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
117
+ primitive, so `init-agents` puts the skill files where they can find them, but
118
+ `lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
119
+ a skill file rather than *invoke* it.
120
+
83
121
  ## Config
84
122
 
85
123
  Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**
@@ -17,7 +17,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
17
17
 
18
18
  0. Do not read the entire code base. Focus on the tasks in this description.
19
19
  1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "planned" is a task.
20
- 2. Read the progress log at `progress.txt` (check Codebase Patterns section first)
20
+ 2. Read the progress log at `progress.txt` **if it exists** (check Codebase Patterns section first) — it is absent until the first slice is built, which is not an error; create it when you write your first entry
21
21
  3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
22
22
  5. Pick the **highest priority** slice where status is **exactly** "Planned" (case insensitive). This becomes your PRD. Set the status "InProgress" in the index.json **and** update the slice status on the eventmodelers board using the `update-slice-status` skill (or MCP if available).
23
23
  **IMPORTANT: Only work on slices with status "Planned" in the CURRENT context. Never pick up a slice that is "InProgress", "Done", "Blocked", "Created", or any other status — even if it looks incomplete. If no slice has status "Planned" in the current context, reply with:**