@eventmodelers/cli 2.0.8 → 2.0.10

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
@@ -162,7 +162,7 @@ npx @eventmodelers/cli run # start the agent loop (ralp
162
162
  npx @eventmodelers/cli run --all # every build kit in the project at once (this directory and up to two levels below, e.g. a scaffolded app's root and web/), prefixed output, Ctrl+c stops all; other flags (--agent, --handover, …) go to each
163
163
  npx @eventmodelers/cli run --all --modeling # the same, plus a modeling agent running alongside the build kits
164
164
  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
165
- npx @eventmodelers/cli run --agent <name> # pick the agent: claude (default), opencode, codex, gemini — bare --agent lists them with their exact commands
165
+ npx @eventmodelers/cli run --agent <name> # pick the agent: claude (default), opencode, codex, gemini, hermes, cursor — bare --agent lists them with their exact commands
166
166
  npx @eventmodelers/cli run --agent <name> --model <id> # …on a given model: -m for opencode/codex/gemini, --model for Claude, LOCAL_AI_MODEL for --local-ai
167
167
  npx @eventmodelers/cli run --exec "<command>" # manual fallback for any other harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
168
168
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
@@ -243,6 +243,7 @@ registered for it (`opencode.json`, `.gemini/settings.json`, or `-c` flags for C
243
243
  npx @eventmodelers/cli run --standalone --agent opencode # = --exec "opencode run --standalone --auto"
244
244
  npx @eventmodelers/cli run --modeling --agent codex # = --exec "codex exec --full-auto"
245
245
  npx @eventmodelers/cli run --modeling --agent gemini # = --exec "gemini --yolo -p"
246
+ npx @eventmodelers/cli run --modeling --agent cursor # = --exec "agent -p --output-format text --approve-mcps --force --trust"; creates one Cursor chat and resumes it every turn, so only the first turn reads the kit
246
247
  npx @eventmodelers/cli run --modeling --agent claude # the default, spelled out
247
248
  npx @eventmodelers/cli run --worker --agent opencode --model opencode/big-pickle # pick the model (a free OpenCode Zen one here)
248
249
  npx @eventmodelers/cli run --agent # list the agents and the command each runs
package/cli.js CHANGED
@@ -44,6 +44,7 @@ import { prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from
44
44
  import { fold } from './shared/build-kit/lib/slice-state.js';
45
45
  import { filterEvents, baselineIds, formatEvent, formatPayload, sliceTitles, summarize } from './shared/build-kit/lib/events-view.js';
46
46
  import { stableGlobalAgentId } from './lib/global-agent-id.js';
47
+ import { AGENT_NAMES, presetCommand, withModel } from './shared/build-kit/lib/harnesses/index.js';
47
48
  import { ensureLearningsGitRules } from './shared/build-kit/lib/ralph.js';
48
49
  import { createCallTracker, heldPromptsRequest, journalPromptAction, shouldJournalCall, journalPromptFinished, journalPromptReceived, recoverOwnPrompts } from './shared/build-kit/lib/prompt-journal.js';
49
50
  import { INFO_KINDS, infoComment, recordInfo, recordTurnInfos } from './shared/build-kit/lib/slice-info.js';
@@ -95,23 +96,13 @@ function resolveLocalAiTarget(opts) {
95
96
  // build kit wants the JSON mode, the only one that reports the usage agent tracing records.
96
97
  // claude has no command — it is the default runner, named so it can be picked explicitly.
97
98
  // --exec stays the fallback for any harness or flag set not listed here.
98
- const AGENT_PRESETS = {
99
- claude: null,
100
- // --standalone: a plain `opencode run` hands the turn to opencode's shared background service,
101
- // started with some other env — so the MCP header's {env:EVENTMODELERS_TOKEN} resolves to nothing
102
- // there and every eventmodelers tool fails to authenticate. A private server inherits ours.
103
- opencode: { modeling: 'opencode run --standalone --auto', build: 'opencode run --standalone --auto --format json' },
104
- codex: { modeling: 'codex exec --full-auto', build: 'codex exec --json --full-auto' },
105
- gemini: { modeling: 'gemini --yolo -p', build: 'gemini --yolo --output-format stream-json -p' },
106
- // -q takes the prompt, so the preset ends with it (the --model flag is inserted before it, below).
107
- // -Q is the plain-text mode a modeling turn reads its answer from; --format stream-json is the build kit's.
108
- hermes: { modeling: 'hermes chat --yolo -Q -q', build: 'hermes chat --yolo --format stream-json -q' },
109
- };
99
+ // The commands live with their harness, in shared/build-kit/lib/harnesses/<name>.js.
110
100
 
111
101
  function printAgentPresets() {
112
102
  console.log('Supported agents for run --agent <name>:\n');
113
- for (const [name, preset] of Object.entries(AGENT_PRESETS)) {
114
- console.log(` ${name.padEnd(9)}${preset ? `modeling: ${preset.modeling}\n ${''.padEnd(9)}build: ${preset.build}` : 'the default runner (warm Claude process / ralph-claude.js)'}`);
103
+ for (const name of AGENT_NAMES) {
104
+ const modeling = presetCommand(name, 'modeling');
105
+ console.log(` ${name.padEnd(9)}${modeling ? `modeling: ${modeling}\n ${''.padEnd(9)}build: ${presetCommand(name, 'build')}` : 'the default runner (warm Claude process / ralph-claude.js)'}`);
115
106
  }
116
107
  console.log('\nAny other harness: run --exec "<command>".');
117
108
  console.log(`A local or self-hosted model: run --local-ai [${LOCAL_AI_TARGETS.join('|')}].`);
@@ -126,8 +117,8 @@ function resolveAgentPreset(opts, loop) {
126
117
  process.exit(0);
127
118
  }
128
119
  const name = String(opts.agent).toLowerCase();
129
- if (!(name in AGENT_PRESETS)) {
130
- console.error(`❌ Unknown --agent "${opts.agent}" — one of: ${Object.keys(AGENT_PRESETS).join(', ')}.`);
120
+ if (!AGENT_NAMES.includes(name)) {
121
+ console.error(`❌ Unknown --agent "${opts.agent}" — one of: ${AGENT_NAMES.join(', ')}.`);
131
122
  console.error(' Any other harness works via --exec "<command>".');
132
123
  console.error(` A local or self-hosted model works via --local-ai [${LOCAL_AI_TARGETS.join('|')}].`);
133
124
  process.exit(1);
@@ -140,13 +131,7 @@ function resolveAgentPreset(opts, loop) {
140
131
  console.error('❌ --agent is mutually exclusive with --local-ai — pick one runner.');
141
132
  process.exit(1);
142
133
  }
143
- const cmd = AGENT_PRESETS[name]?.[loop] ?? null;
144
- // The docker images set this: Codex's own sandbox (landlock) can't start inside a container, and the
145
- // container already is the sandbox — the flag Codex documents for externally sandboxed environments.
146
- if (name === 'codex' && cmd && process.env.EVENTMODELERS_CODEX_NO_SANDBOX === '1') {
147
- return cmd.replace('--full-auto', '--dangerously-bypass-approvals-and-sandbox');
148
- }
149
- return cmd;
134
+ return presetCommand(name, loop);
150
135
  }
151
136
 
152
137
  const STACKS = {
@@ -2167,7 +2152,7 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
2167
2152
  if (localRunner) return body;
2168
2153
  // Every exec turn is a fresh process — a new session that has read nothing, so each one gets
2169
2154
  // the header its first turn would.
2170
- if (!firstTurn && !execRunner) return body;
2155
+ if (!firstTurn && !(execRunner && !execRunner.keepsSession)) return body;
2171
2156
  firstTurn = false;
2172
2157
  return `MODE=modeling token=$EVENTMODELERS_TOKEN org=${cfg.organizationId} baseUrl=${cfg.baseUrl} standalone=${standalone ? 'on' : 'off'}${standalone ? ` max_agents=${maxAgents}` : ''} subagent_model=${subagentModel}\n\n${QUESTIONING_RULE}${execRunner?.mcpRegistered ? EXEC_CONNECTED : ''}Read .agent-modeling-kit/CLAUDE.md now and follow it for every prompt in this session — it's a one-time read; don't re-read it on later turns.\n\n${body}`;
2173
2158
  }
@@ -2414,7 +2399,8 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
2414
2399
  if (!standalone) return (warmUp = Promise.resolve());
2415
2400
  log('warm-up: connecting and listing chapters before the first turn (chapters are read on first use)');
2416
2401
  warmingUp = true;
2417
- warmUp = sendTurn(buildWarmUpTurn())
2402
+ // An exec runner with a session of its own (Cursor's chat) is warmed through that session.
2403
+ warmUp = (execRunner ? execRunner.runTurn(buildWarmUpTurn()) : sendTurn(buildWarmUpTurn()))
2418
2404
  .then((result) => log(`warm-up done — ${oneLine(result, 200) || 'session ready'}`))
2419
2405
  .catch((err) => {
2420
2406
  // Not fatal: put the session header back so the next real turn carries the
@@ -2460,10 +2446,10 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
2460
2446
  // Both warm-ups are the same bet — pay the session's fixed setup cost before a turn arrives
2461
2447
  // rather than making whoever sends the first prompt wait for it. For a local model that cost
2462
2448
  // is the MCP tool set; for Claude it is reading CLAUDE.md and running /connect.
2463
- // An exec runner has nothing to warm: every turn is its own process, so a warm-up would connect
2464
- // a session that exits right after.
2449
+ // An exec runner without a session has nothing to warm: every turn is its own process, so a warm-up
2450
+ // would connect one that exits right after. One with a session (Cursor's chat) does — the chat outlives the process.
2465
2451
  if (localRunner) localRunner.warmUp().catch((err) => log(`local-ai warm-up failed (the first turn will retry): ${err.message}`));
2466
- else if (!execRunner) warmUpSession();
2452
+ else if (!execRunner || execRunner.hasSession) warmUpSession();
2467
2453
 
2468
2454
  async function getRealtimeToken() {
2469
2455
  const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`, {
@@ -3813,8 +3799,8 @@ credentialFlags(program
3813
3799
  .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')
3814
3800
  .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.`)
3815
3801
  .addOption(new Option('--bash').hideHelp())
3816
- .option('--agent [name]', `Pick the agent that does the work by name: ${Object.keys(AGENT_PRESETS).join(', ')}. claude is the default runner; any other is a shortcut for --exec with that harness's headless command filled in — plain-text output for --modeling/--standalone/--worker, JSON output (for agent tracing) for a build kit. Bare --agent lists the agents and the exact command each runs, then stops. Mutually exclusive with --exec and --local-ai.`)
3817
- .option('--model <id>', 'The model the agent runs on, for this run only: `-m <id>` on the --agent opencode/codex/gemini command (e.g. opencode/big-pickle, gpt-5-codex, gemini-2.5-pro), `--model <id>` for Claude (overriding `model` from config), LOCAL_AI_MODEL for --local-ai. Not with --exec — put the harness\'s own model flag in the command there.')
3802
+ .option('--agent [name]', `Pick the agent that does the work by name: ${AGENT_NAMES.join(', ')}. claude is the default runner; any other is a shortcut for --exec with that harness's headless command filled in — plain-text output for --modeling/--standalone/--worker, JSON output (for agent tracing) for a build kit. Bare --agent lists the agents and the exact command each runs, then stops. Mutually exclusive with --exec and --local-ai.`)
3803
+ .option('--model <id>', 'The model the agent runs on, for this run only: `-m <id>` on the --agent opencode/codex/gemini command (`--model <id>` for cursor) (e.g. opencode/big-pickle, gpt-5-codex, gemini-2.5-pro), `--model <id>` for Claude (overriding `model` from config), LOCAL_AI_MODEL for --local-ai. Not with --exec — put the harness\'s own model flag in the command there.')
3818
3804
  .option('--exec [command]', 'Fallback for any harness --agent does not cover, or other flags than its preset. Hand each prompt to an external agent command instead of the default Claude runner — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto", "gemini --yolo -p" or "opencode run". One process per prompt; 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. A build kit runs it via ralph-exec.js; there, 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. With --modeling/--standalone/--worker it is how any agent takes board prompts and chats: the loop claims each prompt or chat message and runs the command once for it. Keep the harness in its plain-text output mode there — stdout is the turn\'s answer — the eventmodelers MCP server is registered for it on every run (opencode.json, .gemini/settings.json, or -c flags for codex; any other command logs what to register by hand), authenticating with $EVENTMODELERS_TOKEN from the env the command inherits, and `init-agents --hosts <harness>` installs the skills for a harness that does not read .claude/skills. No subagents: self-directed work is done inline.')
3819
3805
  .option('--all', 'Run every build kit in this project at once — the one in this directory and any up to two levels below it (a scaffolded app has `.build-kit` at the root and in `web/`, so this works from the app or from the folder holding it) — each from its own directory, output prefixed with the directory, Ctrl+c stops them all. Every other flag is passed on to each run. Not interactive: the kits\' credentials must already be configured (except with --force, which asks once per kit up front, here, before the runs start). With --modeling, the modeling agent runs alongside the build kits. Not with --standalone, --worker, --global, --local or --id (one id cannot belong to several agents).')
3820
3806
  .option('--modeling', 'Connect a modeling agent to the board: it takes prompts and chat messages and hands each to an AI — by default one Claude process kept warm across prompts, for low-latency voice/live use; --agent (or --local-ai, or --exec as the manual fallback) picks another. 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.')
@@ -3902,9 +3888,7 @@ credentialFlags(program
3902
3888
  process.exit(1);
3903
3889
  }
3904
3890
  if (model && agentExec) {
3905
- const modelFlag = `-m '${model.replace(/'/g, `'\\''`)}'`;
3906
- // A trailing -q takes the prompt as its value, so the model flag has to come before it.
3907
- opts.exec = agentExec.endsWith(' -q') ? `${agentExec.slice(0, -3)}${modelFlag} -q` : `${agentExec} ${modelFlag}`;
3891
+ opts.exec = withModel(agentExec, model);
3908
3892
  }
3909
3893
  else if (model && resolveLocalAiTarget(opts) !== null) process.env.LOCAL_AI_MODEL = model;
3910
3894
  // The default Claude runner: the build kit's ralph-claude.js reads RALPH_MODEL from the env it
@@ -22,7 +22,7 @@ import { mkdtempSync, writeFileSync } from 'fs';
22
22
  import { tmpdir } from 'os';
23
23
  import { join } from 'path';
24
24
  import { turnTimeoutMs, superviseTurn } from '../shared/build-kit/lib/turn.js';
25
- import { shellQuote, harnessOf, registerMcp } from '../shared/build-kit/lib/mcp-register.js';
25
+ import { shellQuote, harnessOf, harnessFor, registerMcp } from '../shared/build-kit/lib/harnesses/index.js';
26
26
 
27
27
  export { harnessOf, registerMcp };
28
28
 
@@ -31,6 +31,19 @@ export function createModelingExecRunner({ cfg, command: rawCommand, projectDir,
31
31
  throw new Error('no agent command — pass one, e.g. --exec "codex exec --full-auto", or persist it as localAi.exec in .eventmodelers/config.json');
32
32
  }
33
33
  const { command, registered: mcpRegistered } = registerMcp(rawCommand, projectDir, cfg.baseUrl, log);
34
+ // A harness with a session of its own (Cursor's chat) keeps what it read across turns: the process
35
+ // is still one per turn, but its turns share that session — see harnesses/cursor.js. Such a
36
+ // harness takes its turns one at a time (two turns resuming one session would race on it) and
37
+ // the kit preamble is only skipped once a turn has completed in it.
38
+ const session = harnessFor(rawCommand)?.session ?? null;
39
+ let sessionId = null;
40
+ let primed = false;
41
+ let queue = Promise.resolve();
42
+ function withSession(cmd) {
43
+ if (!session) return cmd;
44
+ if (sessionId === null) sessionId = session.start(cmd, { cwd: projectDir, env, log });
45
+ return sessionId ? session.resume(cmd, sessionId) : cmd;
46
+ }
34
47
  const timeoutMs = turnTimeoutMs(cfg);
35
48
  const promptDir = mkdtempSync(join(tmpdir(), 'modeling-exec-'));
36
49
  const env = {
@@ -42,6 +55,13 @@ export function createModelingExecRunner({ cfg, command: rawCommand, projectDir,
42
55
  };
43
56
 
44
57
  function runTurn(text) {
58
+ if (!session) return runTurnNow(text);
59
+ const result = queue.then(() => runTurnNow(text));
60
+ queue = result.catch(() => {});
61
+ return result;
62
+ }
63
+
64
+ function runTurnNow(text) {
45
65
  return new Promise((resolveTurn, rejectTurn) => {
46
66
  const promptFile = join(promptDir, 'prompt.md');
47
67
  writeFileSync(promptFile, text);
@@ -49,7 +69,7 @@ export function createModelingExecRunner({ cfg, command: rawCommand, projectDir,
49
69
  // stdin ignored: some harnesses append a piped stdin to the prompt, and nobody is there to
50
70
  // type into it. stdout is echoed as it comes — the harness owns its output format, there is
51
71
  // no cross-harness stream to condense — and kept as the turn's result.
52
- const proc = spawn(`${command} ${shellQuote(text)}`, {
72
+ const proc = spawn(`${withSession(command)} ${shellQuote(text)}`, {
53
73
  cwd: projectDir,
54
74
  stdio: ['ignore', 'pipe', 'inherit'],
55
75
  shell: true,
@@ -65,6 +85,7 @@ export function createModelingExecRunner({ cfg, command: rawCommand, projectDir,
65
85
  const timeout = turn.timeoutError();
66
86
  if (timeout) return rejectTurn(timeout);
67
87
  if (code !== 0) return rejectTurn(new Error(`exec command exited ${code}`));
88
+ if (sessionId) primed = true;
68
89
  log(`done (${Date.now() - startedAt}ms)`);
69
90
  resolveTurn(out.trim());
70
91
  });
@@ -74,9 +95,11 @@ export function createModelingExecRunner({ cfg, command: rawCommand, projectDir,
74
95
 
75
96
  return {
76
97
  runTurn,
77
- // Nothing to warm: every turn is its own process.
78
- warmUp: async () => {},
98
+ // Only a harness with a session has anything to warm (the loop sends its warm-up turn through runTurn).
99
+ hasSession: Boolean(session),
79
100
  describe: () => rawCommand,
80
101
  mcpRegistered,
102
+ // True once a turn has succeeded in the shared session — until then every turn still carries the preamble.
103
+ get keepsSession() { return primed; },
81
104
  };
82
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
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": {
@@ -0,0 +1,25 @@
1
+ // OpenAI Codex CLI (`codex exec`).
2
+
3
+ import { shellQuote } from './util.js';
4
+
5
+ export default {
6
+ name: 'codex',
7
+ matches: (words) => words.slice(0, 3).some((w) => w.split('/').pop().replace(/@.*$/, '') === 'codex'),
8
+ preset(loop, env = process.env) {
9
+ const p = { modeling: 'codex exec --full-auto', build: 'codex exec --json --full-auto' };
10
+ // The docker images set this: Codex's own sandbox (landlock) can't start inside a container, and the
11
+ // container already is the sandbox — the flag Codex documents for externally sandboxed environments.
12
+ if (env.EVENTMODELERS_CODEX_NO_SANDBOX === '1') {
13
+ return { modeling: p.modeling.replace('--full-auto', '--dangerously-bypass-approvals-and-sandbox'), build: p.build.replace('--full-auto', '--dangerously-bypass-approvals-and-sandbox') };
14
+ }
15
+ return p;
16
+ },
17
+
18
+ // Codex only loads a project config for a trusted project, so the server rides on the command as flags.
19
+ registerMcp: (command, _projectDir, url) => ({
20
+ // -c values are TOML; env_http_headers names the env var each header is read from.
21
+ command: `${command} -c ${shellQuote(`mcp_servers.eventmodelers.url="${url}"`)} -c ${shellQuote('mcp_servers.eventmodelers.env_http_headers={x-token="EVENTMODELERS_TOKEN",x-agent-id="EVENTMODELERS_AGENT_ID"}')}`,
22
+ file: null,
23
+ manual: null,
24
+ }),
25
+ };
@@ -0,0 +1,62 @@
1
+ // Cursor's CLI (`cursor-agent`, or plain `agent` on current installs).
2
+ //
3
+ // The one harness with a session of its own. The CLI takes one prompt per process, but it can keep
4
+ // a chat across runs: `create-chat` once, `--resume <id>` on every turn. The process is still new
5
+ // each turn; what carries over is the conversation — the kit's CLAUDE.md and the MCP tool set it
6
+ // has already read — so only the first turn pays for them.
7
+
8
+ import { execFileSync, spawnSync } from 'child_process';
9
+ import { join } from 'path';
10
+ import { mergeJsonConfig } from './util.js';
11
+
12
+ const binName = (word) => word.split('/').pop().replace(/@.*$/, '');
13
+
14
+ // Whichever Cursor binary is on PATH; with neither, `cursor-agent` so the failure names a real binary.
15
+ export function cursorBinary() {
16
+ for (const bin of ['cursor-agent', 'agent']) {
17
+ try { execFileSync('which', [bin], { stdio: 'ignore' }); return bin; } catch {}
18
+ }
19
+ return 'cursor-agent';
20
+ }
21
+
22
+ export default {
23
+ name: 'cursor',
24
+ // `agent` is far too generic a word to match anywhere but first.
25
+ matches: (words) => binName(words[0]) === 'agent' || words.slice(0, 3).some((w) => ['cursor-agent', 'cursor'].includes(binName(w))),
26
+ preset() {
27
+ const bin = cursorBinary();
28
+ return {
29
+ modeling: `${bin} -p --output-format text --approve-mcps --force --trust`,
30
+ build: `${bin} -p --output-format stream-json --approve-mcps --force --trust`,
31
+ };
32
+ },
33
+ modelFlag: '--model', // `-m` is an unknown option to this CLI
34
+
35
+ // The same project .cursor/mcp.json the editor reads; ${env:NAME} is resolved from the env the
36
+ // runner hands it. --approve-mcps in the command is what lets a headless run load it unprompted.
37
+ registerMcp(command, projectDir, url) {
38
+ const entry = { url, headers: { 'x-token': '${env:EVENTMODELERS_TOKEN}', 'x-agent-id': '${env:EVENTMODELERS_AGENT_ID}' } };
39
+ const ok = mergeJsonConfig(join(projectDir, '.cursor', 'mcp.json'), (config) => {
40
+ config.mcpServers = config.mcpServers || {};
41
+ config.mcpServers.eventmodelers = entry;
42
+ });
43
+ return { command, file: ok ? '.cursor/mcp.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .cursor/mcp.json` };
44
+ },
45
+
46
+ // Turns share one chat. start() makes it (returns its id, or '' when it could not); resume() puts
47
+ // it on a command — right after the binary, ahead of the flags and the prompt.
48
+ session: {
49
+ start(command, { cwd, env, log }) {
50
+ const bin = command.trim().split(/\s+/)[0];
51
+ const r = spawnSync(bin, ['create-chat'], { cwd, encoding: 'utf8', env });
52
+ const id = String(r.stdout || '').trim().split(/\s+/).pop();
53
+ if (r.status !== 0 || !id) {
54
+ log(`cursor: could not create a chat (${String(r.stderr || '').trim() || `exit ${r.status}`}) — running without resume, every turn starts cold`);
55
+ return '';
56
+ }
57
+ log(`cursor: resuming chat ${id} on every turn`);
58
+ return id;
59
+ },
60
+ resume: (command, id) => command.replace(/^(\s*\S+)/, `$1 --resume ${id}`),
61
+ },
62
+ };
@@ -0,0 +1,19 @@
1
+ // Gemini CLI (`gemini -p`).
2
+
3
+ import { join } from 'path';
4
+ import { mergeJsonConfig } from './util.js';
5
+
6
+ export default {
7
+ name: 'gemini',
8
+ matches: (words) => words.slice(0, 3).some((w) => w.split('/').pop().replace(/@.*$/, '') === 'gemini'),
9
+ preset: () => ({ modeling: 'gemini --yolo -p', build: 'gemini --yolo --output-format stream-json -p' }),
10
+
11
+ registerMcp(command, projectDir, url) {
12
+ const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
13
+ const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
14
+ config.mcpServers = config.mcpServers || {};
15
+ config.mcpServers.eventmodelers = entry;
16
+ });
17
+ return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
18
+ },
19
+ };
@@ -0,0 +1,28 @@
1
+ // Hermes (`hermes chat`).
2
+
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+
7
+ export default {
8
+ name: 'hermes',
9
+ matches: (words) => words.slice(0, 3).some((w) => w.split('/').pop().replace(/@.*$/, '') === 'hermes'),
10
+ // -q takes the prompt, so the preset ends with it (the --model flag is inserted before it, see withModel).
11
+ // -Q is the plain-text mode a modeling turn reads its answer from; --format stream-json is the build kit's.
12
+ preset: () => ({ modeling: 'hermes chat --yolo -Q -q', build: 'hermes chat --yolo --format stream-json -q' }),
13
+ withModel: (command, flag) => (command.endsWith(' -q') ? `${command.slice(0, -3)} ${flag} -q` : `${command} ${flag}`),
14
+
15
+ // Hermes reads MCP servers from ~/.hermes/config.yaml only (there is no project-level file), so
16
+ // this never writes it — a user-wide file is not ours to rewrite. It reports the server as
17
+ // registered when the file already names it (the docker images ship it), and says what to add otherwise.
18
+ registerMcp(command, _projectDir, url) {
19
+ const file = join(process.env.HERMES_HOME || join(homedir(), '.hermes'), 'config.yaml');
20
+ const has = existsSync(file) && /^\s+eventmodelers:/m.test(readFileSync(file, 'utf8'));
21
+ return {
22
+ command,
23
+ file: null,
24
+ reason: '', // not a JSON file — there is nothing to say about its format
25
+ manual: has ? null : `add to ${file}:\nmcp_servers:\n eventmodelers:\n url: "${url}"\n headers:\n x-token: "\${EVENTMODELERS_TOKEN}"`,
26
+ };
27
+ },
28
+ };
@@ -0,0 +1,66 @@
1
+ // The agent harnesses `run --exec` / `run --agent` can hand a turn to, one module each — everything
2
+ // a harness needs (its headless command, how to recognise it, how the eventmodelers MCP server gets
3
+ // registered with it, its model flag, any session of its own) lives in its file. Claude is not here:
4
+ // it has a dedicated runner. Adding a harness is one module and one line in HARNESSES.
5
+ //
6
+ // A module exports: name, matches(words), preset(loop?) -> {modeling, build}, registerMcp(command,
7
+ // projectDir, url) -> {command, file, manual, reason?}, and optionally modelFlag (default `-m`),
8
+ // withModel(command, flag) and session {start, resume}.
9
+
10
+ import opencode from './opencode.js';
11
+ import codex from './codex.js';
12
+ import gemini from './gemini.js';
13
+ import hermes from './hermes.js';
14
+ import cursor from './cursor.js';
15
+ import { shellQuote } from './util.js';
16
+
17
+ export { shellQuote };
18
+ export { opencodeSupportsStandalone, setOpencodeStandaloneSupport } from './opencode.js';
19
+
20
+ const HARNESSES = [opencode, codex, gemini, hermes, cursor];
21
+
22
+ // Every name `--agent` takes, claude (the default runner, no command) first.
23
+ export const AGENT_NAMES = ['claude', ...HARNESSES.map((h) => h.name)];
24
+
25
+ export function harnessByName(name) {
26
+ return HARNESSES.find((h) => h.name === name) ?? null;
27
+ }
28
+
29
+ // Which known harness a command runs — the first words naming one, so `npx @openai/codex exec`
30
+ // and `/opt/bin/opencode run` are recognised as well as the bare names.
31
+ export function harnessFor(command) {
32
+ const words = String(command).trim().split(/\s+/);
33
+ return HARNESSES.find((h) => h.matches(words)) ?? null;
34
+ }
35
+
36
+ export function harnessOf(command) {
37
+ return harnessFor(command)?.name ?? null;
38
+ }
39
+
40
+ // The headless command --agent <name> stands for, for the `modeling` or `build` loop.
41
+ export function presetCommand(name, loop) {
42
+ return harnessByName(name)?.preset(loop)[loop] ?? null;
43
+ }
44
+
45
+ // `--model <id>` on a preset command, spelled the way that harness wants it.
46
+ export function withModel(command, id) {
47
+ const h = harnessFor(command);
48
+ const flag = `${h?.modelFlag ?? '-m'} ${shellQuote(id)}`;
49
+ return h?.withModel ? h.withModel(command, flag) : `${command} ${flag}`;
50
+ }
51
+
52
+ // Registers the MCP server for the command's harness. Returns the command to run with it, and
53
+ // whether the server is now registered — false for an unknown command or a config it could not
54
+ // update, where the turn still has to connect the way the kit says to.
55
+ export function registerMcp(command, projectDir, baseUrl, log) {
56
+ const url = `${baseUrl}/mcp`;
57
+ const harness = harnessFor(command);
58
+ if (!harness) {
59
+ log(`MCP: unknown agent command — register the eventmodelers MCP server with it yourself: ${url}, header x-token from $EVENTMODELERS_TOKEN (and x-agent-id from $EVENTMODELERS_AGENT_ID)`);
60
+ return { command, registered: false };
61
+ }
62
+ const reg = harness.registerMcp(command, projectDir, url);
63
+ if (reg.manual) log(`MCP: could not update the ${harness.name} config${reg.reason ?? ' (not plain JSON)'} — ${reg.manual}`);
64
+ else log(`MCP: eventmodelers registered for ${harness.name}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
65
+ return { command: reg.command, registered: !reg.manual };
66
+ }
@@ -0,0 +1,51 @@
1
+ // OpenCode (`opencode run`).
2
+
3
+ import { spawnSync } from 'child_process';
4
+ import { join } from 'path';
5
+ import { mergeJsonConfig } from './util.js';
6
+
7
+ // Older OpenCode releases have no `run --standalone`; passing it makes `opencode run` print its
8
+ // usage and exit 1, so every turn fails. Ask the installed binary once. Overridable for tests.
9
+ let standaloneSupported = null;
10
+ export function opencodeSupportsStandalone() {
11
+ if (standaloneSupported === null) {
12
+ const r = spawnSync('opencode', ['run', '--help'], { encoding: 'utf8' });
13
+ standaloneSupported = /--standalone\b/.test(`${r.stdout || ''}${r.stderr || ''}`);
14
+ }
15
+ return standaloneSupported;
16
+ }
17
+ export function setOpencodeStandaloneSupport(value) { standaloneSupported = value; }
18
+
19
+ export default {
20
+ name: 'opencode',
21
+ matches: (words) => words.slice(0, 3).some((w) => binName(w) === 'opencode'),
22
+ // --standalone: a plain `opencode run` hands the turn to opencode's shared background service,
23
+ // started with some other env — so the MCP header's {env:EVENTMODELERS_TOKEN} resolves to nothing
24
+ // there and every eventmodelers tool fails to authenticate. A private server inherits ours.
25
+ preset: () => ({ modeling: 'opencode run --standalone --auto', build: 'opencode run --standalone --auto --format json' }),
26
+
27
+ registerMcp(command, projectDir, url) {
28
+ const entry = {
29
+ type: 'remote',
30
+ url,
31
+ headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
32
+ };
33
+ const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
34
+ // OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
35
+ // follow whichever form the file already uses.
36
+ config.mcp = config.mcp || {};
37
+ if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
38
+ else config.mcp.eventmodelers = entry;
39
+ });
40
+ // A plain `opencode run` hands the turn to opencode's shared background service, which was
41
+ // started with some other env: {env:EVENTMODELERS_TOKEN} resolves to nothing there and the
42
+ // server refuses every call. --standalone runs a private server that inherits this one's env.
43
+ // A binary without the flag runs the turn in-process already, so the flag is dropped, not added.
44
+ const supported = opencodeSupportsStandalone();
45
+ const standalone = !supported ? command.replace(/\s--standalone\b/, '')
46
+ : /\s--(standalone|server)\b/.test(` ${command}`) ? command : `${command} --standalone`;
47
+ return { command: standalone, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
48
+ },
49
+ };
50
+
51
+ function binName(word) { return word.split('/').pop().replace(/@.*$/, ''); }
@@ -0,0 +1,24 @@
1
+ // Helpers the harness modules share.
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { dirname } from 'path';
5
+
6
+ // POSIX single-quote escaping: close, insert an escaped quote, reopen. A turn is multi-line
7
+ // text with backticks and $ in it, so it cannot go in unquoted.
8
+ export function shellQuote(s) {
9
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
10
+ }
11
+
12
+ // Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
13
+ // that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
14
+ // the caller says what to add by hand.
15
+ export function mergeJsonConfig(path, apply) {
16
+ let config = {};
17
+ if (existsSync(path)) {
18
+ try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
19
+ }
20
+ apply(config);
21
+ mkdirSync(dirname(path), { recursive: true });
22
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
23
+ return true;
24
+ }
@@ -1,125 +1,5 @@
1
- // Registers the eventmodelers MCP server with an external agent harness (OpenCode, Gemini, Codex, Hermes),
2
- // for both loops that hand turns to one: the modeling loop (lib/modeling-exec.js) and the build
3
- // loop (ralph-exec.js).
4
-
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
6
- import { spawnSync } from 'child_process';
7
- import { homedir } from 'os';
8
- import { dirname, join } from 'path';
9
-
10
- // POSIX single-quote escaping: close, insert an escaped quote, reopen. A turn is multi-line
11
- // text with backticks and $ in it, so it cannot go in unquoted.
12
- export function shellQuote(s) {
13
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
14
- }
15
-
16
- // Older OpenCode releases have no `run --standalone`; passing it makes `opencode run` print its
17
- // usage and exit 1, so every turn fails. Ask the installed binary once. Overridable for tests.
18
- let standaloneSupported = null;
19
- export function opencodeSupportsStandalone() {
20
- if (standaloneSupported === null) {
21
- const r = spawnSync('opencode', ['run', '--help'], { encoding: 'utf8' });
22
- standaloneSupported = /--standalone\b/.test(`${r.stdout || ''}${r.stderr || ''}`);
23
- }
24
- return standaloneSupported;
25
- }
26
- export function setOpencodeStandaloneSupport(value) { standaloneSupported = value; }
27
-
28
- // Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
29
- // that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
30
- // the caller says what to add by hand.
31
- function mergeJsonConfig(path, apply) {
32
- let config = {};
33
- if (existsSync(path)) {
34
- try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
35
- }
36
- apply(config);
37
- mkdirSync(dirname(path), { recursive: true });
38
- writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
39
- return true;
40
- }
41
-
42
- // How each known harness learns about the eventmodelers MCP server — the --exec counterpart of
43
- // ensureMcpRegistered's `.mcp.json` for Claude, and like it rewritten every run, always into the
44
- // harness's *project* config in the agent's working dir (never a user-wide one), and always with
45
- // the token as an env reference the harness resolves from the env this runner hands it, never its
46
- // value. Each entry returns the command to run, which is where a harness without a usable project
47
- // config (Codex only loads one for a trusted project) takes the server as flags instead. Adding a
48
- // harness is one entry here.
49
- const MCP_REGISTRATIONS = {
50
- opencode: (command, projectDir, url) => {
51
- const entry = {
52
- type: 'remote',
53
- url,
54
- headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
55
- };
56
- const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
57
- // OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
58
- // follow whichever form the file already uses.
59
- config.mcp = config.mcp || {};
60
- if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
61
- else config.mcp.eventmodelers = entry;
62
- });
63
- // A plain `opencode run` hands the turn to opencode's shared background service, which was
64
- // started with some other env: {env:EVENTMODELERS_TOKEN} resolves to nothing there and the
65
- // server refuses every call. --standalone runs a private server that inherits this one's env.
66
- // A binary without the flag runs the turn in-process already, so the flag is dropped, not added.
67
- const supported = opencodeSupportsStandalone();
68
- const standalone = !supported ? command.replace(/\s--standalone\b/, '')
69
- : /\s--(standalone|server)\b/.test(` ${command}`) ? command : `${command} --standalone`;
70
- return { command: standalone, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
71
- },
72
- gemini: (command, projectDir, url) => {
73
- const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
74
- const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
75
- config.mcpServers = config.mcpServers || {};
76
- config.mcpServers.eventmodelers = entry;
77
- });
78
- return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
79
- },
80
- // Hermes reads MCP servers from ~/.hermes/config.yaml only (there is no project-level file), so
81
- // this never writes it — a user-wide file is not ours to rewrite. It reports the server as
82
- // registered when the file already names it (the docker images ship it), and says what to add otherwise.
83
- hermes: (command, _projectDir, url) => {
84
- const file = join(process.env.HERMES_HOME || join(homedir(), '.hermes'), 'config.yaml');
85
- const has = existsSync(file) && /^\s+eventmodelers:/m.test(readFileSync(file, 'utf8'));
86
- return {
87
- command,
88
- file: null,
89
- manual: has ? null : `add to ${file}:\nmcp_servers:\n eventmodelers:\n url: "${url}"\n headers:\n x-token: "\${EVENTMODELERS_TOKEN}"`,
90
- };
91
- },
92
- codex: (command, _projectDir, url) => ({
93
- // -c values are TOML; env_http_headers names the env var each header is read from.
94
- command: `${command} -c ${shellQuote(`mcp_servers.eventmodelers.url="${url}"`)} -c ${shellQuote('mcp_servers.eventmodelers.env_http_headers={x-token="EVENTMODELERS_TOKEN",x-agent-id="EVENTMODELERS_AGENT_ID"}')}`,
95
- file: null,
96
- manual: null,
97
- }),
98
- };
99
-
100
- // Which known harness a command runs — the first word naming one, so `npx @openai/codex exec`
101
- // and `/opt/bin/opencode run` are recognised as well as the bare names.
102
- export function harnessOf(command) {
103
- for (const word of String(command).trim().split(/\s+/).slice(0, 3)) {
104
- const name = word.split('/').pop().replace(/@.*$/, '');
105
- if (MCP_REGISTRATIONS[name]) return name;
106
- }
107
- return null;
108
- }
109
-
110
- // Registers the MCP server for the command's harness. Returns the command to run with it, and
111
- // whether the server is now registered — false for an unknown command or a config it could not
112
- // update, where the turn still has to connect the way the kit says to.
113
- export function registerMcp(command, projectDir, baseUrl, log) {
114
- const url = `${baseUrl}/mcp`;
115
- const harness = harnessOf(command);
116
- if (!harness) {
117
- log(`MCP: unknown agent command — register the eventmodelers MCP server with it yourself: ${url}, header x-token from $EVENTMODELERS_TOKEN (and x-agent-id from $EVENTMODELERS_AGENT_ID)`);
118
- return { command, registered: false };
119
- }
120
- const reg = MCP_REGISTRATIONS[harness](command, projectDir, url);
121
- if (reg.manual) log(`MCP: could not update the ${harness} config${harness === 'hermes' ? '' : ' (not plain JSON)'} — ${reg.manual}`);
122
- else log(`MCP: eventmodelers registered for ${harness}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
123
- return { command: reg.command, registered: !reg.manual };
124
- }
1
+ // Registers the eventmodelers MCP server with an external agent harness. The per-harness logic
2
+ // lives in ./harnesses/<name>.js; this module is the stable import for both loops that hand turns
3
+ // to one: the modeling loop (lib/modeling-exec.js) and the build loop (ralph-exec.js).
125
4
 
5
+ export { shellQuote, harnessOf, registerMcp, opencodeSupportsStandalone, setOpencodeStandaloneSupport } from './harnesses/index.js';
@@ -70,7 +70,7 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
70
70
  | `get_image_snapshot_description` | `boardId`, `nodeId` | Load the `{elements:[...]}` sketch description from storage | — (reads what §4 sketch endpoints write) |
71
71
  | `validate_slice_data` | `sliceData` | Offline validation of a `SliceDataOutput` payload — no board access | — (MCP-only, pure function) |
72
72
  | `commit_board_to_git` | `boardId` | Force a git-extension commit/push, bypassing the autoCommit gate | — (MCP-only; git extension) |
73
- | `update_prompt_status` | `promptId`, `newStatus`, `comment?` | Update a prompt's lifecycle status (`ADDED`/`CLAIMED`/`IN_PROGRESS`/`DONE`), optionally with a progress comment. Not board-scoped — no `boardId` arg; the prompt's board is resolved server-side. | §14 `POST .../prompts/:id/status` |
73
+ | `update_prompt_status` | `promptId`, `newStatus`, `comment?` | Update a prompt's lifecycle status (`ADDED`/`CLAIMED`/`IN_PROGRESS`/`DONE`/`FAILED`), optionally with a progress comment. Not board-scoped — no `boardId` arg; the prompt's board is resolved server-side. | §14 `POST .../prompts/:id/status` |
74
74
 
75
75
  **Not exposed via MCP at all** — always use REST/curl for these: §7 Config Import, §10 Snapshots, §11–12 Invitations, §13 Utility (`/api/user`, swagger), and the rest of §14 Prompts (submission, claiming, deletion, realtime-token) — only the status-update endpoint has an MCP tool (`update_prompt_status`, used by the `update-prompt-status` skill); everything else in Prompts is an intentionally separate lifecycle the board-content MCP server doesn't otherwise own.
76
76
 
@@ -1013,7 +1013,7 @@ OpenAPI specification (JSON)
1013
1013
 
1014
1014
  **File**: `src/slices/change/api-prompts/routes.ts`
1015
1015
 
1016
- Prompts are how a human submits work to a modeling agent from the board UI, and how that agent reports its lifecycle back onto the board. Every prompt row has a `status`: `ADDED` (submitted, default) → `CLAIMED` (an agent has picked it up) → `IN_PROGRESS` (an agent is actively working it) → `DONE` (finished). See the `update-prompt-status` skill for the agent-side half of this lifecycle.
1016
+ Prompts are how a human submits work to a modeling agent from the board UI, and how that agent reports its lifecycle back onto the board. Every prompt row has a `status`: `ADDED` (submitted, default) → `CLAIMED` (an agent has picked it up) → `IN_PROGRESS` (an agent is actively working it) → `DONE` (finished) or `FAILED` (not resolved, whether nothing or only part of it could be done — the board keeps the agent task, red, instead of fading it out). See the `update-prompt-status` skill for the agent-side half of this lifecycle.
1017
1017
 
1018
1018
  ### POST `/api/org/:orgId/prompts`
1019
1019
  Submit a prompt for a board timeline. Auth: Supabase JWT (`Authorization: Bearer`).
@@ -30,7 +30,8 @@ Valid `newStatus` values (case-sensitive):
30
30
  | `ADDED` | Default — submitted, not yet claimed. You should never need to set this yourself. |
31
31
  | `CLAIMED` | Already set automatically when `/api/org/:orgId/prompts/next` hands you the prompt — you should never need to set this yourself either. |
32
32
  | `IN_PROGRESS` | You have started working on this prompt. |
33
- | `DONE` | You have finished working on this prompt. |
33
+ | `DONE` | You have finished working on this prompt and it is resolved. |
34
+ | `FAILED` | The prompt is not resolved: you could not do it at all (nothing changed on the board), or only part of it, because it was ambiguous, blocked, or the work did not succeed. The board shows the task red and keeps it instead of fading it out like a `DONE` one. Always give a `comment` saying what is unresolved. |
34
35
 
35
36
  If `newStatus` is not one of these exact values, stop and tell the user the valid options.
36
37
 
@@ -103,7 +103,7 @@ attribute names, `get_node` `projection: "cells"` for a grid and `"edges"` for o
103
103
  `get_slice_data` `projection: "outline"` / `"fields"` / `"specs"` instead of the full graph unless the step needs
104
104
  elements, specs and comments together. The decision table is in `learn-eventmodelers-api` → *Which read to use*.
105
105
 
106
- **Every prompt gets exactly two `/update-prompt-status` calls per turn — never zero, never one.** `IN_PROGRESS` before you start the work (step 4), `DONE` after you finish it (step 6). This holds even for a prompt that turns out to be trivial or a no-op — the board UI has no other way to know the agent picked it up and finished it.
106
+ **Every prompt gets exactly two `/update-prompt-status` calls per turn — never zero, never one.** `IN_PROGRESS` before you start the work (step 4), `DONE` (or `FAILED` if it could not be resolved) after you finish it (step 6). This holds even for a prompt that turns out to be trivial or a no-op — the board UI has no other way to know the agent picked it up and finished it.
107
107
 
108
108
  ## Per-turn steps
109
109
 
@@ -147,8 +147,8 @@ mention it in the `DONE` comment, and leave it for a self-directed turn (or for
147
147
 
148
148
  **Questioning rule**: you are running autonomously — no human is available to answer questions. (A `CHAT` turn never reaches this rule: there someone *is* waiting, and you ask back in the chat — see "Chat turns".) If you need clarification, do not pause or ask interactively — post a comment (`/handle-comment` with `action=place`, `type=COMMENT`) on the most relevant node. Then:
149
149
  - If a reasonable default interpretation exists, continue with it.
150
- - If it doesn't — the prompt is ambiguous enough that any guess risks doing the wrong thing — stop instead of guessing. Skip straight to step 6 and mark the prompt `DONE` with a comment explaining what's unclear and pointing to the comment you just posted. Never leave a prompt neither progressed nor closed.
151
- 6. **Mark the prompt as finished** — invoke `/update-prompt-status` with this turn's `prompt_id`, `newStatus=DONE`, and a `comment` that summarizes what you actually did (e.g. "Added the OrderPlaced event and wired it to the read model"). Do this once, right after the work is done — not per skill call within the turn.
150
+ - If it doesn't — the prompt is ambiguous enough that any guess risks doing the wrong thing — stop instead of guessing. Skip straight to step 6 and mark the prompt `FAILED` with a comment explaining what's unclear and pointing to the comment you just posted. Never leave a prompt neither progressed nor closed.
151
+ 6. **Mark the prompt as finished** — invoke `/update-prompt-status` with this turn's `prompt_id`, `newStatus=DONE`, and a `comment` that summarizes what you actually did (e.g. "Added the OrderPlaced event and wired it to the read model"). If the prompt was not resolved (you could not do it at all, stopped on an ambiguity, hit an error you could not fix, or only part of it was done), use `newStatus=FAILED` instead, with a `comment` saying what is unresolved — a `DONE` prompt disappears from the board, a `FAILED` one stays visible in red. Do this once, right after the work is done — not per skill call within the turn.
152
152
  7. If this turn has a `comment_id` field, invoke `/handle-comment` with `action=resolve`, `nodeId` from the resolved `NODE_ID` (step 3), `commentId` from `comment_id`.
153
153
  8. **`standalone=off` only** — if you noticed something reusable this turn (a pattern, a gotcha, useful context about this model), end your reply with one line per learning: `Learning [<topic>]: <one terse line>` (the topic is optional, at most five lines). The CLI records them — never write them into a file yourself, and add nothing that is already in `.agent-modeling-kit/AGENTS.md`. In a `standalone=on` session skip this: that session writes no files (see Mode), so note anything worth keeping as a board comment instead.
154
154
  **In every session, `standalone=on` too:** anything that is *not* a reusable learning — something you could not change and why, a decision you took — goes the same way as `Info [limitation|decision|note]: <one terse line>` (at most five lines). The CLI records each as an event; never leave it only in prose.