@eventmodelers/cli 1.0.96 → 1.0.98

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 (41) hide show
  1. package/README.md +70 -3
  2. package/cli.js +113 -6
  3. package/lib/modeling-exec.js +4 -86
  4. package/package.json +6 -1
  5. package/shared/build-kit/README.md +22 -4
  6. package/shared/build-kit/lib/mcp-register.js +96 -0
  7. package/shared/build-kit/lib/ralph.js +49 -0
  8. package/shared/build-kit/lib/tracing.js +6 -5
  9. package/shared/build-kit/ralph-claude.js +4 -1
  10. package/shared/build-kit/ralph-exec.js +8 -8
  11. package/shared/skills/learn-eventmodelers-api/SKILL.md +0 -1
  12. package/shared/skills/request-feedback/SKILL.md +23 -32
  13. package/stacks/axon/templates/build-kit/CLAUDE.md +1 -6
  14. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -8
  15. package/stacks/axon/templates/build-kit/lib/prompt.md +2 -2
  16. package/stacks/blank/templates/build-kit/CLAUDE.md +1 -6
  17. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -8
  18. package/stacks/blank/templates/build-kit/lib/prompt.md +2 -9
  19. package/stacks/kurrent/templates/build-kit/CLAUDE.md +1 -6
  20. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -8
  21. package/stacks/kurrent/templates/build-kit/lib/prompt.md +2 -2
  22. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +4 -4
  23. package/stacks/node/templates/build-kit/CLAUDE.md +1 -6
  24. package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -8
  25. package/stacks/node/templates/build-kit/lib/prompt.md +2 -2
  26. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +1 -6
  27. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -8
  28. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +2 -2
  29. package/stacks/react/templates/build-kit/CLAUDE.md +1 -6
  30. package/stacks/react/templates/build-kit/lib/backend-prompt.md +1 -8
  31. package/stacks/react/templates/build-kit/lib/prompt.md +2 -9
  32. package/stacks/supabase/templates/build-kit/CLAUDE.md +1 -6
  33. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -8
  34. package/stacks/supabase/templates/build-kit/lib/prompt.md +2 -2
  35. package/stacks/supabase-react/templates/build-kit/CLAUDE.md +1 -6
  36. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +1 -8
  37. package/stacks/supabase-react/templates/build-kit/lib/prompt.md +2 -9
  38. package/stacks/umadb/templates/build-kit/CLAUDE.md +1 -6
  39. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -8
  40. package/stacks/umadb/templates/build-kit/lib/prompt.md +2 -2
  41. package/stacks/modeling-kit/templates/.claude/settings.local.json +0 -13
package/README.md CHANGED
@@ -128,7 +128,9 @@ npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made de
128
128
  npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
129
129
  npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
130
130
  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
131
- npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
131
+ npx @eventmodelers/cli run --agent <name> # pick the agent: claude (default), opencode, codex, gemini — bare --agent lists them with their exact commands
132
+ 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
133
+ 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"
132
134
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
133
135
  npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
134
136
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
@@ -191,8 +193,43 @@ npx @eventmodelers/cli run --modeling # react to prompts sent to t
191
193
  npx @eventmodelers/cli run --standalone # …and to board changes, on its own initiative
192
194
  ```
193
195
 
194
- `--standalone` implies `--modeling`, so you never need both. To drive either one with a local
195
- model instead of Claude, see [Running the modeling agent on a local model](#running-the-modeling-agent-on-a-local-model).
196
+ `--standalone` implies `--modeling`, so you never need both.
197
+
198
+ **Another coding agent.** `--agent` hands each prompt and chat message to another harness
199
+ instead of the warm Claude process — one process per turn, with the eventmodelers MCP server
200
+ registered for it (`opencode.json`, `.gemini/settings.json`, or `-c` flags for Codex):
201
+
202
+ ```bash
203
+ npx @eventmodelers/cli run --standalone --agent opencode # = --exec "opencode run --standalone --auto"
204
+ npx @eventmodelers/cli run --modeling --agent codex # = --exec "codex exec --full-auto"
205
+ npx @eventmodelers/cli run --modeling --agent gemini # = --exec "gemini --yolo -p"
206
+ npx @eventmodelers/cli run --modeling --agent claude # the default, spelled out
207
+ npx @eventmodelers/cli run --worker --agent opencode --model opencode/big-pickle # pick the model (a free OpenCode Zen one here)
208
+ npx @eventmodelers/cli run --agent # list the agents and the command each runs
209
+ ```
210
+
211
+ `--agent` is only a shortcut: to run a harness it does not know, or the same one with other
212
+ flags, pass the command yourself with `--exec "<command>"`. Keep the harness in its plain-text
213
+ output mode — its stdout is the turn's answer. For OpenCode the loop adds `--standalone` if you
214
+ leave it out — a plain `opencode run` goes to OpenCode's shared background service, which never
215
+ sees `$EVENTMODELERS_TOKEN`, so every eventmodelers tool fails to authenticate. For an unknown command the loop logs the MCP
216
+ server to register by hand (`<baseUrl>/mcp`, header `x-token` from `$EVENTMODELERS_TOKEN`).
217
+ Run `init-agents --hosts <harness>` to install the skills for a harness that does not read
218
+ `.claude/skills`. There are no subagents: self-directed work is done inline.
219
+
220
+ **In a container.** `docker/<agent>/Dockerfile` runs the modeling agent for one coding agent
221
+ (so far `docker/opencode`). Build from this directory and pass the board's credentials as env vars:
222
+
223
+ ```bash
224
+ docker build -f docker/opencode/Dockerfile -t eventmodelers-agent-opencode .
225
+ docker run --rm -e EVENTMODELERS_TOKEN=... -e EVENTMODELERS_ORGANIZATION_ID=... -e EVENTMODELERS_BOARD_ID=... \
226
+ -e OPENCODE_API_KEY=... eventmodelers-agent-opencode --model opencode/big-pickle
227
+ ```
228
+
229
+ `--agent`, `--exec`, `--local-ai` and `--bash` each pick the runner, so pass at most one of them —
230
+ any two together are rejected. For a local or self-hosted model use `--local-ai`, not `--agent`.
231
+
232
+ To drive either one with a local model instead of Claude, see [Running the modeling agent on a local model](#running-the-modeling-agent-on-a-local-model).
196
233
 
197
234
  **No install required.** A modeling agent never touches the directory it was started from —
198
235
  it works against the board over MCP/REST — so it doesn't need a kit scaffolded there. When
@@ -675,6 +712,36 @@ Once your `init --build-kit` scaffold (see above) works against a real backend,
675
712
  2. Add an entry for `<name>` to the `STACKS` object in `cli.js` (`label`, `kitSubdir: 'build-kit'`, `kitDirName: '.build-kit'`, `useShared: true`, `needsBoardId: true`).
676
713
  3. Add it to this README's stack list, the "What gets installed" section, and the `stacks` command's output (generated from `STACKS`, so nothing to add there beyond the entry itself).
677
714
 
715
+ ## Publishing a release
716
+
717
+ Pushing a version tag publishes the npm package **and** the agent docker images from the same commit, via `.github/workflows/release.yml`.
718
+
719
+ 1. Write the release notes (`/release-notes` fills `RELEASE_NOTES.md` from the git history since the last release).
720
+ 2. Bump `version` in `package.json` (`npm version <patch|minor|major> --no-git-tag-version`), and commit it together with the notes.
721
+ 3. Tag that commit with the version, prefixed with `v`, and push the tag:
722
+
723
+ ```bash
724
+ git tag v1.0.98
725
+ git push origin main v1.0.98
726
+ ```
727
+
728
+ The workflow then:
729
+
730
+ | Job | What it does |
731
+ |---|---|
732
+ | `test` | Fails unless the tag equals `package.json`'s version (`v1.0.98` ↔ `1.0.98`), then runs `npm test` |
733
+ | `npm` | `npm publish --access public --provenance` for `@eventmodelers/cli` |
734
+ | `images` | Builds the six agent images for `linux/amd64` and `linux/arm64`, **scans each for credentials** (`.github/scripts/scan-image.sh`), and only then pushes it |
735
+ | `merge` | Combines the two architectures into one multi-arch image per name, tagged `:<tag>` and `:latest` |
736
+
737
+ Images land in `ghcr.io/nebulit-gmbh/`: `eventmodelers-agent-claude`, `eventmodelers-agent-opencode`, and `em-studio-buildkit-{claude,opencode}-{node,java}`. They run the CLI from the tagged checkout, not from npm, so they don't wait on the npm publish.
738
+
739
+ **One-time setup:** an `NPM_TOKEN` repo secret (an npm automation token allowed to publish `@eventmodelers/cli`). The images push with the built-in `GITHUB_TOKEN`; new GHCR packages start private, so make them public in the org's package settings if they should be pullable without a login.
740
+
741
+ A failed run can't be re-published under the same version — npm refuses to overwrite one. If `npm` succeeded but an image job failed, re-run just the failed jobs from the Actions tab. If the tag itself was wrong, delete it (`git push --delete origin v1.0.98`), fix, bump to a new version and tag again.
742
+
743
+ To check an image locally before tagging: `docker build -f docker/<name>/Dockerfile -t scan/<name>:ci .` from this directory, then `../.github/scripts/scan-image.sh scan/<name>:ci`.
744
+
678
745
  ## Contributors
679
746
 
680
747
  | Contributor | Contribution |
package/cli.js CHANGED
@@ -67,6 +67,58 @@ function resolveLocalAiTarget(opts) {
67
67
  return raw;
68
68
  }
69
69
 
70
+ // --- --agent: named shortcuts for --exec ---------------------------------------
71
+ // `run --agent opencode` is exactly `run --exec "<preset>"` — the harness command spelled out
72
+ // once here, so nobody has to remember its headless flags. The two loops want different output
73
+ // from the same harness: the modeling loop reads a turn's answer from plain-text stdout, while a
74
+ // build kit wants the JSON mode, the only one that reports the usage agent tracing records.
75
+ // claude has no command — it is the default runner, named so it can be picked explicitly.
76
+ // --exec stays the fallback for any harness or flag set not listed here.
77
+ const AGENT_PRESETS = {
78
+ claude: null,
79
+ // --standalone: a plain `opencode run` hands the turn to opencode's shared background service,
80
+ // started with some other env — so the MCP header's {env:EVENTMODELERS_TOKEN} resolves to nothing
81
+ // there and every eventmodelers tool fails to authenticate. A private server inherits ours.
82
+ opencode: { modeling: 'opencode run --standalone --auto', build: 'opencode run --standalone --auto --format json' },
83
+ codex: { modeling: 'codex exec --full-auto', build: 'codex exec --json --full-auto' },
84
+ gemini: { modeling: 'gemini --yolo -p', build: 'gemini --yolo --output-format stream-json -p' },
85
+ };
86
+
87
+ function printAgentPresets() {
88
+ console.log('Supported agents for run --agent <name>:\n');
89
+ for (const [name, preset] of Object.entries(AGENT_PRESETS)) {
90
+ console.log(` ${name.padEnd(9)}${preset ? `modeling: ${preset.modeling}\n ${''.padEnd(9)}build: ${preset.build}` : 'the default runner (warm Claude process / ralph-claude.js)'}`);
91
+ }
92
+ console.log('\nAny other harness: run --exec "<command>".');
93
+ console.log(`A local or self-hosted model: run --local-ai [${LOCAL_AI_TARGETS.join('|')}].`);
94
+ }
95
+
96
+ // Resolves --agent into the --exec command it stands for, or null for the default Claude runner.
97
+ // Bare --agent lists the presets and stops.
98
+ function resolveAgentPreset(opts, loop) {
99
+ if (opts.agent === undefined) return null;
100
+ if (opts.agent === true) {
101
+ printAgentPresets();
102
+ process.exit(0);
103
+ }
104
+ const name = String(opts.agent).toLowerCase();
105
+ if (!(name in AGENT_PRESETS)) {
106
+ console.error(`❌ Unknown --agent "${opts.agent}" — one of: ${Object.keys(AGENT_PRESETS).join(', ')}.`);
107
+ console.error(' Any other harness works via --exec "<command>".');
108
+ console.error(` A local or self-hosted model works via --local-ai [${LOCAL_AI_TARGETS.join('|')}].`);
109
+ process.exit(1);
110
+ }
111
+ if (opts.exec) {
112
+ console.error('❌ --agent and --exec are mutually exclusive — --agent is a shortcut for --exec.');
113
+ process.exit(1);
114
+ }
115
+ if (resolveLocalAiTarget(opts) !== null || opts.bash) {
116
+ console.error('❌ --agent is mutually exclusive with --local-ai/--bash — pick one runner.');
117
+ process.exit(1);
118
+ }
119
+ return AGENT_PRESETS[name]?.[loop] ?? null;
120
+ }
121
+
70
122
  const STACKS = {
71
123
  node: {
72
124
  label: 'Node.js / TypeScript',
@@ -1127,6 +1179,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
1127
1179
  // placeholder, never the literal secret (see connect/SKILL.md's Security notes).
1128
1180
  ensureMcpRegistered(targetDir, config.baseUrl || DEFAULT_BASE_URL);
1129
1181
  ensureEnvToken(targetDir, config.token);
1182
+ ensureMcpEnabled(targetDir);
1130
1183
  // Only build stacks trace (slice build cost); they are the ones running the shared runtime.
1131
1184
  if (stackCfg.useShared) await configureAgentTracing(configPath, options.print);
1132
1185
  }
@@ -1317,6 +1370,7 @@ async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
1317
1370
  });
1318
1371
  ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
1319
1372
  ensureEnvToken(targetDir, cfg.token);
1373
+ ensureMcpEnabled(targetDir);
1320
1374
  await configureAgentTracing(configPath, globalOpts.print, true);
1321
1375
  }
1322
1376
 
@@ -1493,6 +1547,25 @@ function ensureMcpRegistered(projectDir, baseUrl) {
1493
1547
  writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
1494
1548
  }
1495
1549
 
1550
+ // `.mcp.json` alone does not make Claude Code use a server: a project server also has to be
1551
+ // approved, and declining that prompt (or dismissing `/mcp`) in an interactive session writes
1552
+ // `disabledMcpjsonServers` into `.claude/settings.local.json`, after which every later `claude` in
1553
+ // the project — the runner's included — silently runs without the eventmodelers tools and the
1554
+ // agent falls back to curl. Whoever starts the runner wants the server on, so this clears that
1555
+ // entry and approves it explicitly. Only this project-local file is touched.
1556
+ function ensureMcpEnabled(projectDir) {
1557
+ const settingsPath = join(projectDir, '.claude', 'settings.local.json');
1558
+ const settings = readJsonSafe(settingsPath);
1559
+ const disabled = settings.disabledMcpjsonServers ?? [];
1560
+ const enabled = settings.enabledMcpjsonServers ?? [];
1561
+ if (!disabled.includes('eventmodelers') && enabled.includes('eventmodelers')) return;
1562
+ settings.disabledMcpjsonServers = disabled.filter((n) => n !== 'eventmodelers');
1563
+ if (!settings.disabledMcpjsonServers.length) delete settings.disabledMcpjsonServers;
1564
+ settings.enabledMcpjsonServers = [...enabled, 'eventmodelers'].filter((n, i, a) => a.indexOf(n) === i);
1565
+ mkdirSync(dirname(settingsPath), { recursive: true });
1566
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1567
+ }
1568
+
1496
1569
  // Companion to ensureMcpRegistered: that function deliberately never writes the
1497
1570
  // token itself, on the assumption the caller sets EVENTMODELERS_TOKEN in whatever
1498
1571
  // process spawns `claude` (true for `run --modeling`'s own spawn). It is NOT true
@@ -1867,7 +1940,7 @@ async function ensureGlobalKit(baseUrl) {
1867
1940
  // untargeted is handed straight back to the queue for another agent to take. It says
1868
1941
  // nothing about the standalone lane — a self-directed turn is nobody's task, so an
1869
1942
  // exclusive standalone agent still works the board on its own initiative.
1870
- async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null } = {}) {
1943
+ async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null, model = null } = {}) {
1871
1944
  // --worker: only prompts — no chat, and no board-change turns (it has nowhere to propose).
1872
1945
  const chat = !worker;
1873
1946
  const configLibPath = join(kitDir, 'lib', 'config.js');
@@ -1886,7 +1959,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
1886
1959
  // run (resolveModelingCredentials) and handed over whole. Walking the filesystem here
1887
1960
  // would also print loadLocalConfig's "no config found — platform sync disabled" note,
1888
1961
  // which is exactly backwards when a complete config was just passed in.
1889
- const local = overrides ? { ...overrides } : loadLocalConfig(kitDir);
1962
+ const local = overrides ? { ...overrides } : applyEnvOverrides(loadLocalConfig(kitDir));
1890
1963
  // The global install's overrides carry their own agent id, kept per board in
1891
1964
  // ~/.eventmodelers/boards/<board>.json — one dir driving several boards must not have
1892
1965
  // them all upsert one shared alive row. A project install keeps its id in the project
@@ -1902,7 +1975,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
1902
1975
  // The identity flags go on last: the global install's `overrides` carry the agent id
1903
1976
  // resolveModelingCredentials just minted for this run, which would otherwise win back over
1904
1977
  // an explicit --id.
1905
- const cfg = { ...(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
1978
+ const cfg = { ...applyEnvOverrides(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}), ...(model ? { model } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
1906
1979
  if (!cfg.boardId) {
1907
1980
  console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
1908
1981
  process.exit(1);
@@ -3503,6 +3576,7 @@ credentialFlags(program
3503
3576
  // (see the beta-api protected-resource-metadata incident this fixed).
3504
3577
  ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
3505
3578
  ensureEnvToken(targetDir, cfg.token);
3579
+ ensureMcpEnabled(targetDir);
3506
3580
  if (existsSync(join(targetDir, STACKS.node.kitDirName))) {
3507
3581
  await configureAgentTracing(configPath, globalOpts.print, !Object.values(overrides).some(Boolean));
3508
3582
  }
@@ -3513,9 +3587,11 @@ credentialFlags(program
3513
3587
  .command('run')
3514
3588
  .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')
3515
3589
  .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.`)
3516
- .option('--exec [command]', '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.')
3590
+ .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, --local-ai and --bash.`)
3591
+ .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.')
3592
+ .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.')
3517
3593
  .option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
3518
- .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; --local-ai or --exec pick 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.')
3594
+ .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.')
3519
3595
  .option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--worker/--global).')
3520
3596
  .option('--standalone', 'Let the modeling agent act on board changes on its own initiative, without asking: it subscribes to the board\'s change channel and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for, 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). Filling that detail in while the human keeps modeling is the point. A plain --modeling agent only proposes, and only to the people chatting with it: after their edits it says in the chat what it would do and waits for a yes; everyone else\'s edits are ignored. Implies --modeling.')
3521
3597
  .option('--max-agents <n>', 'Cap how many subagents a self-directed --standalone turn may dispatch at once, to bound what an unattended agent can spend per turn. The agent merges work that shares a slice or chain first, then takes the most valuable pieces up to this many and leaves the rest for a later turn. 1 makes it do the single most valuable piece itself, without spawning anything. Default 5. Ignored without --standalone — prompt turns are one piece of work by definition.', '5')
@@ -3534,6 +3610,25 @@ credentialFlags(program
3534
3610
  // should fail on the spot, not once the loop is already up — and a cap passed where
3535
3611
  // nothing will read it is worth saying out loud rather than ignoring silently.
3536
3612
  const maxAgents = parseMaxAgents(opts.maxAgents);
3613
+ // --agent is --exec with the command filled in for the loop this run picks.
3614
+ const agentExec = resolveAgentPreset(opts, (opts.modeling || opts.standalone || opts.worker || opts.global) ? 'modeling' : 'build');
3615
+ if (agentExec) opts.exec = agentExec;
3616
+ // --model: one flag, spelled the way each runner takes it. A hand-written --exec command is
3617
+ // the one place it can't go — where the model flag goes in an unknown command is the author's.
3618
+ const model = opts.model === undefined ? null : String(opts.model).trim();
3619
+ if (model === '') {
3620
+ console.error('❌ --model needs a non-empty value.');
3621
+ process.exit(1);
3622
+ }
3623
+ if (model && opts.exec && !agentExec) {
3624
+ console.error('❌ --model does not apply to --exec — put the harness\'s own model flag in the command (e.g. --exec "opencode run --standalone -m <id>"), or use --agent.');
3625
+ process.exit(1);
3626
+ }
3627
+ if (model && agentExec) opts.exec = `${agentExec} -m '${model.replace(/'/g, `'\\''`)}'`;
3628
+ else if (model && resolveLocalAiTarget(opts) !== null) process.env.LOCAL_AI_MODEL = model;
3629
+ // The default Claude runner: the build kit's ralph-claude.js reads RALPH_MODEL from the env it
3630
+ // inherits; the modeling loop takes it as runModeling's `model`.
3631
+ else if (model) process.env.RALPH_MODEL = model;
3537
3632
  if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
3538
3633
  console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
3539
3634
  }
@@ -3644,7 +3739,7 @@ credentialFlags(program
3644
3739
  const runnerLabel = modelingLocalAi ? 'local model' : opts.exec ? 'one agent process per turn' : 'warm Claude process';
3645
3740
  await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
3646
3741
  try {
3647
- await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null });
3742
+ await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null, model: !opts.exec && modelingLocalAi === null ? model : null });
3648
3743
  } catch (err) {
3649
3744
  console.error('[modeling] Fatal:', err);
3650
3745
  process.exit(1);
@@ -3683,6 +3778,18 @@ credentialFlags(program
3683
3778
  process.exit(1);
3684
3779
  }
3685
3780
 
3781
+ // ralph-claude.js hands every turn to a `claude` that only discovers MCP servers from
3782
+ // `.mcp.json` at its own startup, so it is rewritten on every run (see ensureMcpRegistered) —
3783
+ // a file deleted, or written for another baseUrl, since `init` would otherwise leave the agent
3784
+ // without its MCP tools. The token needs no file here: the runner puts it in claude's env.
3785
+ if (runner === 'ralph-claude.js' && !opts.local) {
3786
+ const runCfg = loadEffectiveConfig(cwd, kitDir, globalOpts.config).config;
3787
+ if (runCfg.token) {
3788
+ ensureMcpRegistered(dirname(kitDir), runCfg.baseUrl || DEFAULT_BASE_URL);
3789
+ ensureMcpEnabled(dirname(kitDir));
3790
+ }
3791
+ }
3792
+
3686
3793
  console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
3687
3794
  const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
3688
3795
  try {
@@ -18,95 +18,13 @@
18
18
  // reply. So run the harness in its plain-text mode, not a JSON one.
19
19
 
20
20
  import { spawn } from 'child_process';
21
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'fs';
21
+ import { mkdtempSync, writeFileSync } from 'fs';
22
22
  import { tmpdir } from 'os';
23
- import { dirname, join } from 'path';
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
26
 
26
- // POSIX single-quote escaping: close, insert an escaped quote, reopen. A turn is multi-line
27
- // text with backticks and $ in it, so it cannot go in unquoted.
28
- function shellQuote(s) {
29
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
30
- }
31
-
32
- // Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
33
- // that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
34
- // the caller says what to add by hand.
35
- function mergeJsonConfig(path, apply) {
36
- let config = {};
37
- if (existsSync(path)) {
38
- try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
39
- }
40
- apply(config);
41
- mkdirSync(dirname(path), { recursive: true });
42
- writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
43
- return true;
44
- }
45
-
46
- // How each known harness learns about the eventmodelers MCP server — the --exec counterpart of
47
- // ensureMcpRegistered's `.mcp.json` for Claude, and like it rewritten every run, always into the
48
- // harness's *project* config in the agent's working dir (never a user-wide one), and always with
49
- // the token as an env reference the harness resolves from the env this runner hands it, never its
50
- // value. Each entry returns the command to run, which is where a harness without a usable project
51
- // config (Codex only loads one for a trusted project) takes the server as flags instead. Adding a
52
- // harness is one entry here.
53
- const MCP_REGISTRATIONS = {
54
- opencode: (command, projectDir, url) => {
55
- const entry = {
56
- type: 'remote',
57
- url,
58
- headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
59
- };
60
- const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
61
- // OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
62
- // follow whichever form the file already uses.
63
- config.mcp = config.mcp || {};
64
- if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
65
- else config.mcp.eventmodelers = entry;
66
- });
67
- return { command, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
68
- },
69
- gemini: (command, projectDir, url) => {
70
- const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
71
- const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
72
- config.mcpServers = config.mcpServers || {};
73
- config.mcpServers.eventmodelers = entry;
74
- });
75
- return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
76
- },
77
- codex: (command, _projectDir, url) => ({
78
- // -c values are TOML; env_http_headers names the env var each header is read from.
79
- 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"}')}`,
80
- file: null,
81
- manual: null,
82
- }),
83
- };
84
-
85
- // Which known harness a command runs — the first word naming one, so `npx @openai/codex exec`
86
- // and `/opt/bin/opencode run` are recognised as well as the bare names.
87
- export function harnessOf(command) {
88
- for (const word of String(command).trim().split(/\s+/).slice(0, 3)) {
89
- const name = word.split('/').pop().replace(/@.*$/, '');
90
- if (MCP_REGISTRATIONS[name]) return name;
91
- }
92
- return null;
93
- }
94
-
95
- // Registers the MCP server for the command's harness. Returns the command to run with it, and
96
- // whether the server is now registered — false for an unknown command or a config it could not
97
- // update, where the turn still has to connect the way the kit says to.
98
- export function registerMcp(command, projectDir, baseUrl, log) {
99
- const url = `${baseUrl}/mcp`;
100
- const harness = harnessOf(command);
101
- if (!harness) {
102
- 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)`);
103
- return { command, registered: false };
104
- }
105
- const reg = MCP_REGISTRATIONS[harness](command, projectDir, url);
106
- if (reg.manual) log(`MCP: could not update the ${harness} config (not plain JSON) — ${reg.manual}`);
107
- else log(`MCP: eventmodelers registered for ${harness}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
108
- return { command: reg.command, registered: !reg.manual };
109
- }
27
+ export { harnessOf, registerMcp };
110
28
 
111
29
  export function createModelingExecRunner({ cfg, command: rawCommand, projectDir, log }) {
112
30
  if (!rawCommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.96",
3
+ "version": "1.0.98",
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": {
@@ -39,5 +39,10 @@
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=18.0.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Nebulit-GmbH/Eventmodelers-Build-Kits.git",
46
+ "directory": "eventmodelers-cli"
42
47
  }
43
48
  }
@@ -94,7 +94,7 @@ rewriting a full coding agent inside the kit, so it is left out on purpose.
94
94
 
95
95
  ```bash
96
96
  npx @eventmodelers/cli run --exec "codex exec --oss --json --full-auto" # Codex on local Ollama
97
- npx @eventmodelers/cli run --exec "opencode run --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
97
+ npx @eventmodelers/cli run --exec "opencode run --standalone --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
98
98
  ```
99
99
 
100
100
  Or keep Claude Code as the builder and point `anthropicBaseUrl` at a local endpoint that
@@ -121,16 +121,34 @@ llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
121
121
 
122
122
  Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
123
123
  are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
124
- already is one and only wants a prompt. They go through `ralph-exec.js` instead:
124
+ already is one and only wants a prompt. They go through `ralph-exec.js` instead. The
125
+ known ones have a shortcut that fills in the command, JSON flag for agent tracing included
126
+ (bare `--agent` lists them):
127
+
128
+ ```bash
129
+ npx @eventmodelers/cli run --agent opencode # = --exec "opencode run --standalone --auto --format json"
130
+ npx @eventmodelers/cli run --agent codex # = --exec "codex exec --json --full-auto"
131
+ npx @eventmodelers/cli run --agent gemini # = --exec "gemini --yolo --output-format stream-json -p"
132
+ ```
133
+
134
+ Add `--model <id>` to pick the model — it becomes the harness's `-m <id>`
135
+ (`run --agent opencode --model opencode/big-pickle`); on the default Claude runner it
136
+ overrides `model` from config for that run.
137
+
138
+ `--exec` is the manual fallback: any other harness, or one of these with other flags:
125
139
 
126
140
  ```bash
127
141
  npx @eventmodelers/cli run --exec "codex exec --full-auto"
128
- npx @eventmodelers/cli run --exec "opencode run"
142
+ npx @eventmodelers/cli run --exec "opencode run --standalone --auto" # --standalone: see below
129
143
 
130
144
  # …or persist it and use the bare flag
131
145
  RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
132
146
  ```
133
147
 
148
+ Run OpenCode with `--standalone`: a plain `opencode run` hands the turn to OpenCode's shared
149
+ background service, which was started with another environment and never sees the variables
150
+ this runner sets (`EVENTMODELERS_TOKEN`, `RALPH_PROMPT_FILE`, …).
151
+
134
152
  The prompt is appended to the command as one shell-quoted argument, and is also written
135
153
  to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
136
154
  child runs with the project dir as its cwd and inherits stdio — a harness owns its own
@@ -144,7 +162,7 @@ so without the flag a turn is not traced (the runner logs a hint). Add the flag
144
162
 
145
163
  ```bash
146
164
  npx @eventmodelers/cli run --exec "codex exec --json --full-auto"
147
- npx @eventmodelers/cli run --exec "opencode run --auto --format json"
165
+ npx @eventmodelers/cli run --exec "opencode run --standalone --auto --format json"
148
166
  npx @eventmodelers/cli run --exec "gemini --yolo --output-format stream-json -p"
149
167
  ```
150
168
 
@@ -0,0 +1,96 @@
1
+ // Registers the eventmodelers MCP server with an external agent harness (OpenCode, Gemini, Codex),
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 { dirname, join } from 'path';
7
+
8
+ // POSIX single-quote escaping: close, insert an escaped quote, reopen. A turn is multi-line
9
+ // text with backticks and $ in it, so it cannot go in unquoted.
10
+ export function shellQuote(s) {
11
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
12
+ }
13
+
14
+ // Adds or refreshes one key in a project JSON config, leaving everything else in it alone. A file
15
+ // that isn't plain JSON (a JSONC one with comments) is not rewritten — returns false instead, and
16
+ // the caller says what to add by hand.
17
+ function mergeJsonConfig(path, apply) {
18
+ let config = {};
19
+ if (existsSync(path)) {
20
+ try { config = JSON.parse(readFileSync(path, 'utf8')); } catch { return false; }
21
+ }
22
+ apply(config);
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
25
+ return true;
26
+ }
27
+
28
+ // How each known harness learns about the eventmodelers MCP server — the --exec counterpart of
29
+ // ensureMcpRegistered's `.mcp.json` for Claude, and like it rewritten every run, always into the
30
+ // harness's *project* config in the agent's working dir (never a user-wide one), and always with
31
+ // the token as an env reference the harness resolves from the env this runner hands it, never its
32
+ // value. Each entry returns the command to run, which is where a harness without a usable project
33
+ // config (Codex only loads one for a trusted project) takes the server as flags instead. Adding a
34
+ // harness is one entry here.
35
+ const MCP_REGISTRATIONS = {
36
+ opencode: (command, projectDir, url) => {
37
+ const entry = {
38
+ type: 'remote',
39
+ url,
40
+ headers: { 'x-token': '{env:EVENTMODELERS_TOKEN}', 'x-agent-id': '{env:EVENTMODELERS_AGENT_ID}' },
41
+ };
42
+ const ok = mergeJsonConfig(join(projectDir, 'opencode.json'), (config) => {
43
+ // OpenCode 2 nests servers under mcp.servers and still reads the older flat mcp.<name>;
44
+ // follow whichever form the file already uses.
45
+ config.mcp = config.mcp || {};
46
+ if (config.mcp.servers) config.mcp.servers.eventmodelers = entry;
47
+ else config.mcp.eventmodelers = entry;
48
+ });
49
+ // A plain `opencode run` hands the turn to opencode's shared background service, which was
50
+ // started with some other env: {env:EVENTMODELERS_TOKEN} resolves to nothing there and the
51
+ // server refuses every call. --standalone runs a private server that inherits this one's env.
52
+ const standalone = /\s--(standalone|server)\b/.test(` ${command}`) ? command : `${command} --standalone`;
53
+ return { command: standalone, file: ok ? 'opencode.json' : null, manual: ok ? null : `add mcp.eventmodelers = ${JSON.stringify(entry)} to opencode.json` };
54
+ },
55
+ gemini: (command, projectDir, url) => {
56
+ const entry = { httpUrl: url, headers: { 'x-token': '$EVENTMODELERS_TOKEN', 'x-agent-id': '$EVENTMODELERS_AGENT_ID' } };
57
+ const ok = mergeJsonConfig(join(projectDir, '.gemini', 'settings.json'), (config) => {
58
+ config.mcpServers = config.mcpServers || {};
59
+ config.mcpServers.eventmodelers = entry;
60
+ });
61
+ return { command, file: ok ? '.gemini/settings.json' : null, manual: ok ? null : `add mcpServers.eventmodelers = ${JSON.stringify(entry)} to .gemini/settings.json` };
62
+ },
63
+ codex: (command, _projectDir, url) => ({
64
+ // -c values are TOML; env_http_headers names the env var each header is read from.
65
+ 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"}')}`,
66
+ file: null,
67
+ manual: null,
68
+ }),
69
+ };
70
+
71
+ // Which known harness a command runs — the first word naming one, so `npx @openai/codex exec`
72
+ // and `/opt/bin/opencode run` are recognised as well as the bare names.
73
+ export function harnessOf(command) {
74
+ for (const word of String(command).trim().split(/\s+/).slice(0, 3)) {
75
+ const name = word.split('/').pop().replace(/@.*$/, '');
76
+ if (MCP_REGISTRATIONS[name]) return name;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ // Registers the MCP server for the command's harness. Returns the command to run with it, and
82
+ // whether the server is now registered — false for an unknown command or a config it could not
83
+ // update, where the turn still has to connect the way the kit says to.
84
+ export function registerMcp(command, projectDir, baseUrl, log) {
85
+ const url = `${baseUrl}/mcp`;
86
+ const harness = harnessOf(command);
87
+ if (!harness) {
88
+ 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)`);
89
+ return { command, registered: false };
90
+ }
91
+ const reg = MCP_REGISTRATIONS[harness](command, projectDir, url);
92
+ if (reg.manual) log(`MCP: could not update the ${harness} config (not plain JSON) — ${reg.manual}`);
93
+ else log(`MCP: eventmodelers registered for ${harness}${reg.file ? ` in ${reg.file}` : ' via -c flags'}`);
94
+ return { command: reg.command, registered: !reg.manual };
95
+ }
96
+